diff --git a/CHANGES.md b/CHANGES.md
index 93a9de8a8..1a9713d0a 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -29,6 +29,7 @@
 - Improved share link options. Now you can allow non-team members to comment and/or inspect [Taiga #3056] (https://tree.taiga.io/project/penpot/us/3056)
 - Signin/Signup from shared link [Taiga #3472](https://tree.taiga.io/project/penpot/us/3472)
 - Support for import/export binary format [Taiga #2991](https://tree.taiga.io/project/penpot/us/2991)
+- Comments positioning [Taiga #https://2007](tree.taiga.io/project/penpot/us/2007)
 
 ### :bug: Bugs fixed
 
diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj
index a861fe218..086147ad2 100644
--- a/backend/src/app/migrations.clj
+++ b/backend/src/app/migrations.clj
@@ -238,6 +238,9 @@
 
    {:name "0076-mod-storage-object-table"
     :fn (mg/resource "app/migrations/sql/0076-mod-storage-object-table.sql")}
+    
+   {:name "0077-mod-comment-thread-table"
+    :fn (mg/resource "app/migrations/sql/0077-mod-comment-thread-table.sql")}
    ])
 
 
diff --git a/backend/src/app/migrations/sql/0077-mod-comment-thread-table.sql b/backend/src/app/migrations/sql/0077-mod-comment-thread-table.sql
new file mode 100644
index 000000000..7898ca2cf
--- /dev/null
+++ b/backend/src/app/migrations/sql/0077-mod-comment-thread-table.sql
@@ -0,0 +1,3 @@
+--- Add frame_id field.
+ALTER TABLE comment_thread
+  ADD COLUMN frame_id uuid NULL DEFAULT '00000000-0000-0000-0000-000000000000';
diff --git a/backend/src/app/rpc/mutations/comments.clj b/backend/src/app/rpc/mutations/comments.clj
index e8a60c8b7..feda6566b 100644
--- a/backend/src/app/rpc/mutations/comments.clj
+++ b/backend/src/app/rpc/mutations/comments.clj
@@ -31,9 +31,10 @@
 (s/def ::profile-id ::us/uuid)
 (s/def ::position ::gpt/point)
 (s/def ::content ::us/string)
+(s/def ::frame-id ::us/uuid)
 
 (s/def ::create-comment-thread
-  (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id]
+  (s/keys :req-un [::profile-id ::file-id ::position ::content ::page-id ::frame-id]
           :opt-un [::share-id]))
 
 (sv/defmethod ::create-comment-thread
@@ -53,7 +54,7 @@
     (:next-seqn res)))
 
 (defn- create-comment-thread
-  [conn {:keys [profile-id file-id page-id position content] :as params}]
+  [conn {:keys [profile-id file-id page-id position content frame-id] :as params}]
   (let [seqn    (retrieve-next-seqn conn file-id)
         now     (dt/now)
         pname   (retrieve-page-name conn params)
@@ -66,7 +67,8 @@
                             :created-at now
                             :modified-at now
                             :seqn seqn
-                            :position (db/pgpoint position)})]
+                            :position (db/pgpoint position)
+                            :frame-id frame-id})]
 
 
     ;; Create a comment entry
@@ -281,3 +283,40 @@
                   :code :not-allowed))
 
       (db/delete! conn :comment {:id id}))))
+
+;; --- Mutation: Update comment thread position
+
+(s/def ::update-comment-thread-position
+  (s/keys :req-un [::profile-id ::id ::position ::frame-id]))
+
+(sv/defmethod ::update-comment-thread-position
+  [{:keys [pool] :as cfg} {:keys [profile-id id position frame-id] :as params}]
+  (db/with-atomic [conn pool]
+    (let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
+      (when-not (= (:owner-id thread) profile-id)
+        (ex/raise :type :validation
+                  :code :not-allowed))
+      (db/update! conn :comment-thread
+                  {:modified-at (dt/now)
+                   :position (db/pgpoint position)
+                   :frame-id frame-id}
+                  {:id (:id thread)})
+      nil)))
+
+;; --- Mutation: Update comment frame
+
+(s/def ::update-comment-thread-frame
+  (s/keys :req-un [::profile-id ::id ::frame-id]))
+
+(sv/defmethod ::update-comment-thread-frame
+  [{:keys [pool] :as cfg} {:keys [profile-id id frame-id] :as params}]
+  (db/with-atomic [conn pool]
+    (let [thread (db/get-by-id conn :comment-thread id {:for-update true})]
+      (when-not (= (:owner-id thread) profile-id)
+        (ex/raise :type :validation
+                  :code :not-allowed))
+      (db/update! conn :comment-thread
+                  {:modified-at (dt/now)
+                   :frame-id frame-id}
+                  {:id (:id thread)})
+      nil)))
diff --git a/frontend/resources/styles/main/partials/comments.scss b/frontend/resources/styles/main/partials/comments.scss
index 7019ee9c3..e3bd0904e 100644
--- a/frontend/resources/styles/main/partials/comments.scss
+++ b/frontend/resources/styles/main/partials/comments.scss
@@ -29,6 +29,9 @@
     &.unread {
       background-color: $color-primary;
     }
+    span {
+      user-select: none;
+    }
   }
 
   .thread-content {
@@ -77,7 +80,7 @@
       resize: none;
       width: 100%;
       border-radius: 2px;
-      border: 1px solid $color-gray-10;
+      border: 1px solid $color-gray-20;
       max-height: 4rem;
     }
 
@@ -188,6 +191,7 @@
         margin: 0 $size-2 0 26px;
         white-space: pre-wrap;
         display: inline-block;
+        word-break: break-all;
       }
     }
   }
diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs
index 8d749e5f3..5490e0743 100644
--- a/frontend/src/app/main/data/comments.cljs
+++ b/frontend/src/app/main/data/comments.cljs
@@ -8,7 +8,10 @@
   (:require
    [app.common.data :as d]
    [app.common.geom.point :as gpt]
+   [app.common.pages.helpers :as cph]
    [app.common.spec :as us]
+   [app.common.uuid :as uuid]
+   [app.main.data.workspace.state-helpers :as wsh]
    [app.main.repo :as rp]
    [beicon.core :as rx]
    [cljs.spec.alpha :as s]
@@ -59,29 +62,70 @@
 (declare retrieve-comment-threads)
 (declare refresh-comment-thread)
 
-(s/def ::create-thread-params
+(s/def ::create-thread-on-workspace-params
   (s/keys :req-un [::page-id ::file-id ::position ::content]))
 
-(defn create-thread
-  [params]
-  (us/assert ::create-thread-params params)
-  (letfn [(created [{:keys [id comment] :as thread} state]
-            (-> state
-                (update :comment-threads assoc id (dissoc thread :comment))
-                (update :comments-local assoc :open id)
-                (update :comments-local dissoc :draft)
-                (update :workspace-drawing dissoc :comment)
-                (update-in [:comments id] assoc (:id comment) comment)))]
+(s/def ::create-thread-on-viewer-params
+  (s/keys :req-un [::page-id ::file-id ::position ::content ::frame-id]))
 
-    (ptk/reify ::create-comment-thread
-      ptk/WatchEvent
-      (watch [_ state _]
-         (let [share-id (-> state :viewer-local :share-id)
-               params (assoc params :share-id share-id)]
-           (->> (rp/mutation :create-comment-thread params)
-                (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %) :share-id share-id}))
-                (rx/map #(partial created %))
-                (rx/catch #(rx/throw {:type :comment-error}))))))))
+(defn created-thread-on-workspace
+  [{:keys [id comment page-id] :as thread}]
+
+  (ptk/reify ::created-thread-on-workspace
+    ptk/UpdateEvent
+    (update [_ state]
+      (-> state
+          (update :comment-threads assoc id (dissoc thread :comment))
+          (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id (select-keys thread [:position :frame-id]))
+          (update :comments-local assoc :open id)
+          (update :comments-local dissoc :draft)
+          (update :workspace-drawing dissoc :comment)
+          (update-in [:comments id] assoc (:id comment) comment)))))
+
+(defn create-thread-on-workspace
+  [params]
+  (us/assert ::create-thread-on-workspace-params params)
+
+  (ptk/reify ::create-thread-on-workspace
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [page-id (:current-page-id state)
+            objects (wsh/lookup-page-objects state page-id)
+            frame-id (cph/frame-id-by-position objects (:position params))
+            params (assoc params :frame-id frame-id)]
+        (->> (rp/mutation :create-comment-thread params)
+             (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %)}))
+             (rx/map created-thread-on-workspace)
+             (rx/catch #(rx/throw {:type :comment-error})))))))
+
+(defn created-thread-on-viewer
+  [{:keys [id comment page-id] :as thread}]
+
+  (ptk/reify ::created-thread-on-workspace
+    ptk/UpdateEvent
+    (update [_ state]
+      (-> state
+          (update :comment-threads assoc id (dissoc thread :comment))
+          (update-in [:viewer :pages page-id :options :comment-threads-position] assoc id (select-keys thread [:position :frame-id]))
+          (update :comments-local assoc :open id)
+          (update :comments-local dissoc :draft)
+          (update :workspace-drawing dissoc :comment)
+          (update-in [:comments id] assoc (:id comment) comment)))))
+
+(defn create-thread-on-viewer
+  [params]
+  (us/assert ::create-thread-on-viewer-params params)
+
+  (ptk/reify ::create-thread-on-viewer
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [share-id (-> state :viewer-local :share-id)
+            frame-id (:frame-id params)
+            params (assoc params :share-id share-id :frame-id frame-id)]
+        (->> (rp/mutation :create-comment-thread params)
+             (rx/mapcat #(rp/query :comment-thread {:file-id (:file-id %) :id (:id %) :share-id share-id}))
+             (rx/map created-thread-on-viewer)
+             (rx/catch #(rx/throw {:type :comment-error})))))))
 
 (defn update-comment-thread-status
   [{:keys [id] :as thread}]
@@ -95,7 +139,6 @@
              (rx/map (constantly done))
              (rx/catch #(rx/throw {:type :comment-error})))))))
 
-
 (defn update-comment-thread
   [{:keys [id is-resolved] :as thread}]
   (us/assert ::comment-thread thread)
@@ -114,7 +157,6 @@
               (rx/catch #(rx/throw {:type :comment-error}))
               (rx/ignore))))))
 
-
 (defn add-comment
   [thread content]
   (us/assert ::comment-thread thread)
@@ -146,15 +188,35 @@
              (rx/catch #(rx/throw {:type :comment-error}))
              (rx/ignore))))))
 
-(defn delete-comment-thread
+(defn delete-comment-thread-on-workspace
   [{:keys [id] :as thread}]
   (us/assert ::comment-thread thread)
-  (ptk/reify ::delete-comment-thread
+  (ptk/reify ::delete-comment-thread-on-workspace
     ptk/UpdateEvent
     (update [_ state]
-      (-> state
-          (update :comments dissoc id)
-          (update :comment-threads dissoc id)))
+      (let [page-id (:current-page-id state)]
+        (-> state
+            (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] dissoc id)
+            (update :comments dissoc id)
+            (update :comment-threads dissoc id))))
+
+    ptk/WatchEvent
+    (watch [_ _ _]
+      (->> (rp/mutation :delete-comment-thread {:id id})
+           (rx/catch #(rx/throw {:type :comment-error}))
+           (rx/ignore)))))
+
+(defn delete-comment-thread-on-viewer
+  [{:keys [id] :as thread}]
+  (us/assert ::comment-thread thread)
+  (ptk/reify ::delete-comment-thread-on-viewer
+    ptk/UpdateEvent
+    (update [_ state]
+      (let [page-id (:current-page-id state)]
+        (-> state
+            (update-in [:viewer :pages page-id :options :comment-threads-position] dissoc id)
+            (update :comments dissoc id)
+            (update :comment-threads dissoc id))))
 
     ptk/WatchEvent
     (watch [_ state _]
@@ -194,8 +256,18 @@
 (defn retrieve-comment-threads
   [file-id]
   (us/assert ::us/uuid file-id)
-  (letfn [(fetched [data state]
-            (assoc state :comment-threads (d/index-by :id data)))]
+  (letfn [(set-comment-threds [state comment-thread]
+            (let [path [:workspace-data :pages-index (:page-id comment-thread) :options :comment-threads-position (:id comment-thread)]
+                  thread-position (get-in state path)]
+              (cond-> state
+                (nil? thread-position)
+                (->
+                 (assoc-in (conj path :position) (:position comment-thread))
+                 (assoc-in (conj path :frame-id) (:frame-id comment-thread))))))
+           (fetched [data state]
+             (let [state (assoc state :comment-threads (d/index-by :id data))]
+               (reduce set-comment-threds state data)))]
+
     (ptk/reify ::retrieve-comment-threads
       ptk/WatchEvent
       (watch [_ state _]
@@ -338,3 +410,41 @@
 
       (= :yours mode)
       (filter #(contains? (:participants %) (:id profile))))))
+
+(defn update-comment-thread-frame
+    ([thread ]
+   (update-comment-thread-frame thread uuid/zero))
+
+   ([thread frame-id]
+    (us/assert ::comment-thread thread)
+    (ptk/reify ::update-comment-thread-frame
+      ptk/UpdateEvent
+      (update [_ state]
+        (let [thread-id (:id thread)]
+          (assoc-in state [:comment-threads thread-id :frame-id] frame-id)))
+
+      ptk/WatchEvent
+      (watch [_ _ _]
+        (let [thread-id (:id thread)]
+          (->> (rp/mutation :update-comment-thread-frame {:id thread-id  :frame-id frame-id})
+               (rx/catch #(rx/throw {:type :comment-error :code :update-comment-thread-frame}))
+               (rx/ignore)))))))
+
+(defn detach-comment-thread
+  "Detach comment threads that are inside a frame when that frame is deleted"
+  [ids]
+  (us/verify (s/coll-of uuid?) ids)
+
+  (ptk/reify ::detach-comment-thread
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [objects (wsh/lookup-page-objects state)
+            is-frame? (fn [id] (= :frame (get-in objects [id :type])))
+            frame-ids? (into #{} (filter is-frame?) ids)]
+
+        (->> state
+             :comment-threads
+             (vals)
+             (filter (fn [comment] (some #(= % (:frame-id comment)) frame-ids?)))
+             (map update-comment-thread-frame)
+             (rx/from))))))
diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs
index 8d2abf1c8..397d3824e 100644
--- a/frontend/src/app/main/data/workspace.cljs
+++ b/frontend/src/app/main/data/workspace.cljs
@@ -13,7 +13,6 @@
    [app.common.geom.point :as gpt]
    [app.common.geom.proportions :as gpr]
    [app.common.geom.shapes :as gsh]
-   [app.common.math :as mth]
    [app.common.pages :as cp]
    [app.common.pages.changes-builder :as pcb]
    [app.common.pages.helpers :as cph]
@@ -50,6 +49,7 @@
    [app.main.data.workspace.thumbnails :as dwth]
    [app.main.data.workspace.transforms :as dwt]
    [app.main.data.workspace.undo :as dwu]
+   [app.main.data.workspace.viewport :as dwv]
    [app.main.data.workspace.zoom :as dwz]
    [app.main.repo :as rp]
    [app.main.streams :as ms]
@@ -397,140 +397,6 @@
         (assoc-in state [:workspace-global :tooltip] content)
         (assoc-in state [:workspace-global :tooltip] nil)))))
 
-;; --- Viewport Sizing
-
-(defn initialize-viewport
-  [{:keys [width height] :as size}]
-  (letfn [(update* [{:keys [vport] :as local}]
-            (let [wprop (/ (:width vport) width)
-                  hprop (/ (:height vport) height)]
-              (-> local
-                  (assoc :vport size)
-                  (update :vbox (fn [vbox]
-                                  (-> vbox
-                                      (update :width #(/ % wprop))
-                                      (update :height #(/ % hprop))))))))
-
-          (initialize [state local]
-            (let [page-id (:current-page-id state)
-                  objects (wsh/lookup-page-objects state page-id)
-                  shapes  (cph/get-immediate-children objects)
-                  srect   (gsh/selection-rect shapes)
-                  local   (assoc local :vport size :zoom 1)]
-              (cond
-                (or (not (d/num? (:width srect)))
-                    (not (d/num? (:height srect))))
-                (assoc local :vbox (assoc size :x 0 :y 0))
-
-                (or (> (:width srect) width)
-                    (> (:height srect) height))
-                (let [srect (gal/adjust-to-viewport size srect {:padding 40})
-                      zoom  (/ (:width size) (:width srect))]
-                  (-> local
-                      (assoc :zoom zoom)
-                      (update :vbox merge srect)))
-
-                :else
-                (assoc local :vbox (assoc size
-                                          :x (+ (:x srect) (/ (- (:width srect) width) 2))
-                                          :y (+ (:y srect) (/ (- (:height srect) height) 2)))))))
-
-          (setup [state local]
-            (if (and (:vbox local) (:vport local))
-              (update* local)
-              (initialize state local)))]
-
-    (ptk/reify ::initialize-viewport
-      ptk/UpdateEvent
-      (update [_ state]
-        (update state :workspace-local
-                (fn [local]
-                  (setup state local)))))))
-
-(defn update-viewport-position
-  [{:keys [x y] :or {x identity y identity}}]
-  (us/assert fn? x)
-  (us/assert fn? y)
-  (ptk/reify ::update-viewport-position
-    ptk/UpdateEvent
-    (update [_ state]
-      (update-in state [:workspace-local :vbox]
-                 (fn [vbox]
-                   (-> vbox
-                       (update :x x)
-                       (update :y y)))))))
-
-(defn update-viewport-size
-  [resize-type {:keys [width height] :as size}]
-  (ptk/reify ::update-viewport-size
-    ptk/UpdateEvent
-    (update [_ state]
-      (update state :workspace-local
-              (fn [{:keys [vport] :as local}]
-                (if (or (nil? vport)
-                        (mth/almost-zero? width)
-                        (mth/almost-zero? height))
-                  ;; If we have a resize to zero just keep the old value
-                  local
-                  (let [wprop (/ (:width vport) width)
-                        hprop (/ (:height vport) height)
-
-                        vbox (:vbox local)
-                        vbox-x (:x vbox)
-                        vbox-y (:y vbox)
-                        vbox-width (:width vbox)
-                        vbox-height (:height vbox)
-
-                        vbox-width' (/ vbox-width wprop)
-                        vbox-height' (/ vbox-height hprop)
-
-                        vbox-x'
-                        (case resize-type
-                          :left  (+ vbox-x (- vbox-width vbox-width'))
-                          :right vbox-x
-                          (+ vbox-x (/ (- vbox-width vbox-width') 2)))
-
-                        vbox-y'
-                        (case resize-type
-                          :top  (+ vbox-y (- vbox-height vbox-height'))
-                          :bottom vbox-y
-                          (+ vbox-y (/ (- vbox-height vbox-height') 2)))]
-                    (-> local
-                        (assoc :vport size)
-                        (assoc-in [:vbox :x] vbox-x')
-                        (assoc-in [:vbox :y] vbox-y')
-                        (assoc-in [:vbox :width] vbox-width')
-                        (assoc-in [:vbox :height] vbox-height')))))))))
-
-(defn start-panning []
-  (ptk/reify ::start-panning
-    ptk/WatchEvent
-    (watch [_ state stream]
-      (let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
-            zoom (-> (get-in state [:workspace-local :zoom]) gpt/point)]
-        (when-not (get-in state [:workspace-local :panning])
-          (rx/concat
-           (rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
-           (->> stream
-                (rx/filter ms/pointer-event?)
-                (rx/filter #(= :delta (:source %)))
-                (rx/map :pt)
-                (rx/take-until stopper)
-                (rx/map (fn [delta]
-                          (let [delta (gpt/divide delta zoom)]
-                            (update-viewport-position {:x #(- % (:x delta))
-                                                       :y #(- % (:y delta))})))))))))))
-
-(defn finish-panning []
-  (ptk/reify ::finish-panning
-    ptk/UpdateEvent
-    (update [_ state]
-      (-> state
-          (update :workspace-local dissoc :panning)))))
-
-
-
-
 ;; --- Update Shape Attrs
 
 (defn update-shape
@@ -1762,3 +1628,10 @@
 
 ;; Thumbnails
 (dm/export dwth/update-thumbnail)
+
+;; Viewport
+(dm/export dwv/initialize-viewport)
+(dm/export dwv/update-viewport-position)
+(dm/export dwv/update-viewport-size)
+(dm/export dwv/start-panning)
+(dm/export dwv/finish-panning)
diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs
index b7f9f2030..2e8293042 100644
--- a/frontend/src/app/main/data/workspace/comments.cljs
+++ b/frontend/src/app/main/data/workspace/comments.cljs
@@ -6,13 +6,22 @@
 
 (ns app.main.data.workspace.comments
   (:require
+   [app.common.geom.point :as gpt]
+   [app.common.geom.shapes :as gsh]
+   [app.common.pages.changes-builder :as pcb]
+   [app.common.pages.helpers :as cph]
    [app.common.spec :as us]
    [app.main.data.comments :as dcm]
-   [app.main.data.workspace :as dw]
-   [app.main.data.workspace.common :as dwc]
+   [app.main.data.workspace.changes :as dwc]
+   [app.main.data.workspace.common :as dwco]
+   [app.main.data.workspace.drawing :as dwd]
+   [app.main.data.workspace.state-helpers :as wsh]
+   [app.main.data.workspace.viewport :as dwv]
+   [app.main.repo :as rp]
    [app.main.streams :as ms]
    [app.util.router :as rt]
    [beicon.core :as rx]
+   [cljs.spec.alpha :as s]
    [potok.core :as ptk]))
 
 (declare handle-interrupt)
@@ -33,7 +42,7 @@
               (rx/map handle-comment-layer-click)
               (rx/take-until stoper))
          (->> stream
-              (rx/filter dwc/interrupt?)
+              (rx/filter dwco/interrupt?)
               (rx/map handle-interrupt)
               (rx/take-until stoper)))))))
 
@@ -95,8 +104,76 @@
         (rx/merge
          (rx/of (rt/nav :workspace pparams qparams))
          (->> stream
-              (rx/filter (ptk/type? ::dw/initialize-viewport))
+              (rx/filter (ptk/type? ::dwv/initialize-viewport))
               (rx/take 1)
               (rx/mapcat #(rx/of (center-to-comment-thread thread)
-                                 (dw/select-for-drawing :comments)
+                                 (dwd/select-for-drawing :comments)
                                  (dcm/open-thread thread)))))))))
+
+(defn update-comment-thread-position
+  ([thread  [new-x new-y]]
+   (update-comment-thread-position thread  [new-x new-y] nil))
+
+  ([thread  [new-x new-y] frame-id]
+  (us/assert ::dcm/comment-thread thread)
+  (ptk/reify ::update-comment-thread-position
+    ptk/WatchEvent
+    (watch [it state _]
+      (let [thread-id (:id thread)
+            page (wsh/lookup-page state)
+            page-id (:id page)
+            objects (wsh/lookup-page-objects state page-id)
+            new-frame-id (if (nil? frame-id)
+                           (cph/frame-id-by-position objects {:x new-x :y new-y})
+                           (:frame-id thread))
+            thread (assoc thread
+                          :position {:x new-x :y new-y}
+                          :frame-id new-frame-id)
+
+            changes
+            (-> (pcb/empty-changes it)
+                (pcb/with-page page)
+                (pcb/update-page-option :comment-threads-position assoc thread-id (select-keys thread [:position :frame-id])))]
+
+        (rx/merge
+         (rx/of (dwc/commit-changes changes))
+         (->> (rp/mutation :update-comment-thread-position thread)
+              (rx/catch #(rx/throw {:type :update-comment-thread-position}))
+              (rx/ignore))))))))
+
+(defn move-frame-comment-threads
+  "Move comment threads that are inside a frame when that frame is moved"
+  [ids]
+  (us/verify (s/coll-of uuid?) ids)
+
+  (ptk/reify ::move-frame-comment-threads
+    ptk/WatchEvent
+    (watch [_ state _]
+      (let [objects (wsh/lookup-page-objects state)
+
+            is-frame? (fn [id] (= :frame (get-in objects [id :type])))
+            frame-ids? (into #{} (filter is-frame?) ids)
+
+            object-modifiers  (:workspace-modifiers state)
+
+            threads-position-map (:comment-threads-position (wsh/lookup-page-options state))
+
+            build-move-event
+            (fn [comment-thread]
+              (let [frame (get objects (:frame-id comment-thread))
+                    frame' (-> (merge frame (get object-modifiers (:frame-id comment-thread)))
+                               (gsh/transform-shape))
+                    moved (gpt/to-vec (gpt/point (:x frame) (:y frame))
+                                      (gpt/point (:x frame') (:y frame')))
+                    position (get-in threads-position-map [(:id comment-thread) :position])
+                    new-x (+ (:x position) (:x moved))
+                    new-y (+ (:y position) (:y moved))]
+                (update-comment-thread-position comment-thread [new-x new-y] (:id frame))))]
+
+        (->> (:comment-threads state)
+             (vals)
+             (map #(assoc % :position (get-in threads-position-map [(:id %) :position])))
+             (map #(assoc % :frame-id (get-in threads-position-map [(:id %) :frame-id])))
+             (filter (comp frame-ids? :frame-id))
+             (map build-move-event)
+             (rx/from))))))
diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs
index 2b9a2f23b..a072e3023 100644
--- a/frontend/src/app/main/data/workspace/shapes.cljs
+++ b/frontend/src/app/main/data/workspace/shapes.cljs
@@ -17,6 +17,7 @@
    [app.common.types.shape :as spec.shape]
    [app.common.types.shape.interactions :as csi]
    [app.common.uuid :as uuid]
+   [app.main.data.comments :as dc]
    [app.main.data.workspace.changes :as dch]
    [app.main.data.workspace.edition :as dwe]
    [app.main.data.workspace.selection :as dws]
@@ -237,7 +238,9 @@
                                                            (->> (map :id starting-flows)
                                                                 (reduce csp/remove-flow flows))))))]
 
-        (rx/of (dch/commit-changes changes))))))
+        (rx/of
+         (dc/detach-comment-thread ids)
+         (dch/commit-changes changes))))))
 
 (defn- viewport-center
   [state]
diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs
index 5a20b2946..92c193c5f 100644
--- a/frontend/src/app/main/data/workspace/transforms.cljs
+++ b/frontend/src/app/main/data/workspace/transforms.cljs
@@ -18,6 +18,7 @@
    [app.common.spec :as us]
    [app.main.data.workspace.changes :as dch]
    [app.main.data.workspace.collapse :as dwc]
+   [app.main.data.workspace.comments :as dwcm]
    [app.main.data.workspace.guides :as dwg]
    [app.main.data.workspace.selection :as dws]
    [app.main.data.workspace.state-helpers :as wsh]
@@ -201,6 +202,7 @@
             (rx/of (dwu/start-undo-transaction))
             (rx/empty))
           (rx/of (dwg/move-frame-guides ids-with-children)
+                 (dwcm/move-frame-comment-threads ids-with-children)
                  (dch/update-shapes
                   ids-with-children
                   (fn [shape]
diff --git a/frontend/src/app/main/data/workspace/viewport.cljs b/frontend/src/app/main/data/workspace/viewport.cljs
new file mode 100644
index 000000000..aad315aa1
--- /dev/null
+++ b/frontend/src/app/main/data/workspace/viewport.cljs
@@ -0,0 +1,148 @@
+;; This Source Code Form is subject to the terms of the Mozilla Public
+;; License, v. 2.0. If a copy of the MPL was not distributed with this
+;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+;;
+;; Copyright (c) UXBOX Labs SL
+
+(ns app.main.data.workspace.viewport
+  (:require
+   [app.common.data :as d]
+   [app.common.geom.align :as gal]
+   [app.common.geom.point :as gpt]
+   [app.common.geom.shapes :as gsh]
+   [app.common.math :as mth]
+   [app.common.pages.helpers :as cph]
+   [app.common.spec :as us] 
+   [app.main.data.workspace.state-helpers :as wsh]
+   [app.main.streams :as ms]
+   [beicon.core :as rx]
+   [potok.core :as ptk]))
+
+(defn initialize-viewport
+  [{:keys [width height] :as size}]
+  (letfn [(update* [{:keys [vport] :as local}]
+            (let [wprop (/ (:width vport) width)
+                  hprop (/ (:height vport) height)]
+              (-> local
+                  (assoc :vport size)
+                  (update :vbox (fn [vbox]
+                                  (-> vbox
+                                      (update :width #(/ % wprop))
+                                      (update :height #(/ % hprop))))))))
+
+          (initialize [state local]
+            (let [page-id (:current-page-id state)
+                  objects (wsh/lookup-page-objects state page-id)
+                  shapes  (cph/get-immediate-children objects)
+                  srect   (gsh/selection-rect shapes)
+                  local   (assoc local :vport size :zoom 1)]
+              (cond
+                (or (not (d/num? (:width srect)))
+                    (not (d/num? (:height srect))))
+                (assoc local :vbox (assoc size :x 0 :y 0))
+
+                (or (> (:width srect) width)
+                    (> (:height srect) height))
+                (let [srect (gal/adjust-to-viewport size srect {:padding 40})
+                      zoom  (/ (:width size) (:width srect))]
+                  (-> local
+                      (assoc :zoom zoom)
+                      (update :vbox merge srect)))
+
+                :else
+                (assoc local :vbox (assoc size
+                                          :x (+ (:x srect) (/ (- (:width srect) width) 2))
+                                          :y (+ (:y srect) (/ (- (:height srect) height) 2)))))))
+
+          (setup [state local]
+            (if (and (:vbox local) (:vport local))
+              (update* local)
+              (initialize state local)))]
+
+    (ptk/reify ::initialize-viewport
+      ptk/UpdateEvent
+      (update [_ state]
+        (update state :workspace-local
+                (fn [local]
+                  (setup state local)))))))
+
+(defn update-viewport-position
+  [{:keys [x y] :or {x identity y identity}}]
+  (us/assert fn? x)
+  (us/assert fn? y)
+  (ptk/reify ::update-viewport-position
+    ptk/UpdateEvent
+    (update [_ state]
+      (update-in state [:workspace-local :vbox]
+                 (fn [vbox]
+                   (-> vbox
+                       (update :x x)
+                       (update :y y)))))))
+
+(defn update-viewport-size
+  [resize-type {:keys [width height] :as size}]
+  (ptk/reify ::update-viewport-size
+    ptk/UpdateEvent
+    (update [_ state]
+      (update state :workspace-local
+              (fn [{:keys [vport] :as local}]
+                (if (or (nil? vport)
+                        (mth/almost-zero? width)
+                        (mth/almost-zero? height))
+                  ;; If we have a resize to zero just keep the old value
+                  local
+                  (let [wprop (/ (:width vport) width)
+                        hprop (/ (:height vport) height)
+
+                        vbox (:vbox local)
+                        vbox-x (:x vbox)
+                        vbox-y (:y vbox)
+                        vbox-width (:width vbox)
+                        vbox-height (:height vbox)
+
+                        vbox-width' (/ vbox-width wprop)
+                        vbox-height' (/ vbox-height hprop)
+
+                        vbox-x'
+                        (case resize-type
+                          :left  (+ vbox-x (- vbox-width vbox-width'))
+                          :right vbox-x
+                          (+ vbox-x (/ (- vbox-width vbox-width') 2)))
+
+                        vbox-y'
+                        (case resize-type
+                          :top  (+ vbox-y (- vbox-height vbox-height'))
+                          :bottom vbox-y
+                          (+ vbox-y (/ (- vbox-height vbox-height') 2)))]
+                    (-> local
+                        (assoc :vport size)
+                        (assoc-in [:vbox :x] vbox-x')
+                        (assoc-in [:vbox :y] vbox-y')
+                        (assoc-in [:vbox :width] vbox-width')
+                        (assoc-in [:vbox :height] vbox-height')))))))))
+
+(defn start-panning []
+  (ptk/reify ::start-panning
+    ptk/WatchEvent
+    (watch [_ state stream]
+      (let [stopper (->> stream (rx/filter (ptk/type? ::finish-panning)))
+            zoom (-> (get-in state [:workspace-local :zoom]) gpt/point)]
+        (when-not (get-in state [:workspace-local :panning])
+          (rx/concat
+           (rx/of #(-> % (assoc-in [:workspace-local :panning] true)))
+           (->> stream
+                (rx/filter ms/pointer-event?)
+                (rx/filter #(= :delta (:source %)))
+                (rx/map :pt)
+                (rx/take-until stopper)
+                (rx/map (fn [delta]
+                          (let [delta (gpt/divide delta zoom)]
+                            (update-viewport-position {:x #(- % (:x delta))
+                                                       :y #(- % (:y delta))})))))))))))
+
+(defn finish-panning []
+  (ptk/reify ::finish-panning
+    ptk/UpdateEvent
+    (update [_ state]
+      (-> state
+          (update :workspace-local dissoc :panning)))))
diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs
index 5aa993a27..14361b96f 100644
--- a/frontend/src/app/main/ui/comments.cljs
+++ b/frontend/src/app/main/ui/comments.cljs
@@ -9,6 +9,7 @@
    [app.config :as cfg]
    [app.main.data.comments :as dcm]
    [app.main.data.modal :as modal]
+   [app.main.data.workspace.comments :as dwcm]
    [app.main.refs :as refs]
    [app.main.store :as st]
    [app.main.ui.components.dropdown :refer [dropdown]]
@@ -183,7 +184,7 @@
       [:input.btn-secondary {:type "button" :value "Cancel" :on-click on-cancel}]]]))
 
 (mf/defc comment-item
-  [{:keys [comment thread users] :as props}]
+  [{:keys [comment thread users origin] :as props}]
   (let [owner    (get users (:owner-id comment))
         profile  (mf/deref refs/profile)
         options  (mf/use-state false)
@@ -210,7 +211,9 @@
         (mf/use-callback
          (mf/deps thread)
          #(st/emit! (dcm/close-thread)
-                    (dcm/delete-comment-thread thread)))
+                    (if (= origin :viewer)
+                      (dcm/delete-comment-thread-on-viewer thread)
+                      (dcm/delete-comment-thread-on-workspace thread))))
 
 
         on-delete-thread
@@ -278,9 +281,10 @@
   (l/derived (l/in [:comments id]) st/state))
 
 (mf/defc thread-comments
-  [{:keys [thread zoom users]}]
+  [{:keys [thread zoom users origin]}]
   (let [ref   (mf/use-ref)
         pos   (:position thread)
+
         pos-x (+ (* (:x pos) zoom) 14)
         pos-y (- (* (:y pos) zoom) 14)
 
@@ -313,33 +317,134 @@
        [:div.comments
         [:& comment-item {:comment comment
                           :users users
-                          :thread thread}]
+                          :thread thread
+                          :origin origin}]
         (for [item (rest comments)]
           [:*
            [:hr]
-           [:& comment-item {:comment item :users users}]])
+           [:& comment-item {:comment item
+                             :users users
+                             :origin origin}]])
         [:div {:ref ref}]]
        [:& reply-form {:thread thread}]])))
 
+(defn use-buble
+  [zoom {:keys [position frame-id]}]
+  (let [dragging-ref (mf/use-ref false)
+        start-ref (mf/use-ref nil)
+
+        state (mf/use-state {:hover false
+                             :new-position-x nil
+                             :new-position-y nil
+                             :new-frame-id frame-id})
+
+        on-pointer-down
+        (mf/use-callback
+         (fn [event]
+           (dom/capture-pointer event)
+           (mf/set-ref-val! dragging-ref true)
+           (mf/set-ref-val! start-ref (dom/get-client-position event))))
+
+        on-pointer-up
+        (mf/use-callback
+         (mf/deps (select-keys @state [:new-position-x :new-position-y :new-frame-id]))
+         (fn [_ thread]
+           (when (and
+                  (some? (:new-position-x @state))
+                  (some? (:new-position-y @state)))
+             (st/emit! (dwcm/update-comment-thread-position thread [(:new-position-x @state) (:new-position-y @state)])))))
+
+        on-lost-pointer-capture
+        (mf/use-callback
+         (fn [event]
+           (dom/release-pointer event)
+           (mf/set-ref-val! dragging-ref false)
+           (mf/set-ref-val! start-ref nil)
+           (swap! state assoc :new-position-x nil)
+           (swap! state assoc :new-position-y nil)))
+
+        on-mouse-move
+        (mf/use-callback
+         (mf/deps position zoom)
+         (fn [event]
+           (when-let [_ (mf/ref-val dragging-ref)]
+             (let [start-pt (mf/ref-val start-ref)
+                   current-pt (dom/get-client-position event)
+                   delta-x (/ (- (:x current-pt) (:x start-pt)) zoom)
+                   delta-y (/ (- (:y current-pt) (:y start-pt)) zoom)]
+               (swap! state assoc
+                      :new-position-x (+ (:x position) delta-x)
+                      :new-position-y (+ (:y position) delta-y))))))]
+
+    {:on-pointer-down on-pointer-down
+     :on-pointer-up on-pointer-up
+     :on-mouse-move on-mouse-move
+     :on-lost-pointer-capture on-lost-pointer-capture
+     :state state}))
+
 (mf/defc thread-bubble
   {::mf/wrap [mf/memo]}
-  [{:keys [thread zoom on-click] :as params}]
+  [{:keys [thread zoom open? on-click origin]}]
   (let [pos   (:position thread)
-        pos-x (* (:x pos) zoom)
-        pos-y (* (:y pos) zoom)
-        on-click* (fn [event]
-                    (dom/stop-propagation event)
-                    (on-click thread))]
+        drag? (mf/use-ref nil)
+        was-open? (mf/use-ref nil)
+
+        {:keys [on-pointer-down
+                on-pointer-up
+                on-mouse-move
+                state
+                on-lost-pointer-capture]} (use-buble zoom thread)
+
+        pos-x (* (or (:new-position-x @state) (:x pos)) zoom)
+        pos-y (* (or (:new-position-y @state) (:y pos)) zoom)
+
+        on-pointer-down* (mf/use-callback
+                          (mf/deps origin was-open? open? drag? on-pointer-down)
+                          (fn [event]
+                            (when (not= origin :viewer)
+                              (mf/set-ref-val! was-open? open?)
+                              (when open? (st/emit! (dcm/close-thread)))
+                              (mf/set-ref-val! drag? false)
+                              (dom/stop-propagation event)
+                              (on-pointer-down event))))
+
+        on-pointer-up* (mf/use-callback
+                        (mf/deps origin thread was-open? drag? on-pointer-up)
+                        (fn [event]
+                          (when (not= origin :viewer)
+                            (dom/stop-propagation event)
+                            (on-pointer-up event thread)
+
+                            (when (or (and (mf/ref-val was-open?) (mf/ref-val drag?))
+                                      (and (not (mf/ref-val was-open?)) (not (mf/ref-val drag?))))
+                              (st/emit! (dcm/open-thread thread))))))
+
+        on-mouse-move* (mf/use-callback
+                        (mf/deps origin drag? on-mouse-move)
+                        (fn [event]
+                         (when (not= origin :viewer)
+                           (mf/set-ref-val! drag? true)
+                           (dom/stop-propagation event)
+                           (on-mouse-move event))))
+
+        on-click* (mf/use-callback
+                   (mf/deps origin thread on-click)
+                   (fn [event]
+                     (dom/stop-propagation event)
+                     (when (= origin :viewer)
+                       (on-click thread))))]
 
     [:div.thread-bubble
      {:style {:top (str pos-y "px")
               :left (str pos-x "px")}
-      :on-mouse-down (fn [event]
-                       (dom/prevent-default event))
+      :on-pointer-down on-pointer-down*
+      :on-pointer-up on-pointer-up*
+      :on-mouse-move on-mouse-move*
+      :on-click on-click*
+      :on-lost-pointer-capture on-lost-pointer-capture
       :class (dom/classnames
               :resolved (:is-resolved thread)
-              :unread (pos? (:count-unread-comments thread)))
-      :on-click on-click*}
+              :unread (pos? (:count-unread-comments thread)))}
      [:span (:seqn thread)]]))
 
 (mf/defc comment-thread
diff --git a/frontend/src/app/main/ui/viewer/comments.cljs b/frontend/src/app/main/ui/viewer/comments.cljs
index bc69fd66f..c46147ed9 100644
--- a/frontend/src/app/main/ui/viewer/comments.cljs
+++ b/frontend/src/app/main/ui/viewer/comments.cljs
@@ -85,6 +85,8 @@
 (mf/defc comments-layer
   [{:keys [zoom file users frame page] :as props}]
   (let [profile     (mf/deref refs/profile)
+        threads-position-ref  (l/derived (l/in [:viewer :pages (:id page) :options :comment-threads-position]) st/state)
+        threads-position-map  (mf/deref threads-position-ref)
         threads-map (mf/deref threads-ref)
 
         frame-corner (-> frame :points gsh/points->selrect gpt/point)
@@ -96,7 +98,16 @@
 
         cstate      (mf/deref refs/comments-local)
 
+        update-thread-position (fn update-thread-position [thread]
+                                 (if (contains? threads-position-map (:id thread))
+                                   (-> thread
+                                       (assoc :position (get-in threads-position-map [(:id thread) :position]))
+                                       (assoc :frame-id (get-in threads-position-map [(:id thread) :frame-id])))
+                                   thread))
+
         threads     (->> (vals threads-map)
+                         (map update-thread-position)
+                         (filter #(= (:frame-id %) (:id frame)))
                          (dcm/apply-filters cstate profile)
                          (filter (fn [{:keys [position]}]
                                    (gsh/has-point? frame position))))
@@ -135,8 +146,10 @@
         (mf/use-callback
          (mf/deps frame)
          (fn [draft]
-           (let [params (update draft :position gpt/transform modifier2)]
-             (st/emit! (dcm/create-thread params)
+           (let [params (-> draft
+                            (update  :position gpt/transform modifier2)
+                            (assoc :frame-id (:id frame)))]
+             (st/emit! (dcm/create-thread-on-viewer params)
                        (dcm/close-thread)))))]
 
     [:div.comments-section {:on-click on-click}
@@ -148,7 +161,8 @@
                                   :zoom zoom
                                   :on-click on-bubble-click
                                   :open? (= (:id item) (:open cstate))
-                                  :key (:seqn item)}]))
+                                  :key (:seqn item)
+                                  :origin :viewer}]))
 
        (when-let [id (:open cstate)]
          (when-let [thread (as-> (get threads-map id) $
diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs
index 3dfbe8cb8..07f60122d 100644
--- a/frontend/src/app/main/ui/workspace/viewport.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport.cljs
@@ -144,7 +144,7 @@
         on-pointer-move   (actions/on-pointer-move viewport-ref zoom move-stream)
         on-pointer-up     (actions/on-pointer-up)
         on-move-selected  (actions/on-move-selected hover hover-ids selected space?)
-        on-menu-selected  (actions/on-menu-selected hover hover-ids selected)
+        on-menu-selected  (actions/on-menu-selected hover hover-ids selected) 
 
         on-frame-enter    (actions/on-frame-enter frame-hover)
         on-frame-leave    (actions/on-frame-leave frame-hover)
diff --git a/frontend/src/app/main/ui/workspace/viewport/comments.cljs b/frontend/src/app/main/ui/workspace/viewport/comments.cljs
index f13a88cfc..34dd92aeb 100644
--- a/frontend/src/app/main/ui/workspace/viewport/comments.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/comments.cljs
@@ -12,27 +12,32 @@
    [app.main.store :as st]
    [app.main.ui.comments :as cmt]
    [cuerdas.core :as str]
+   [okulary.core :as l]
    [rumext.alpha :as mf]))
 
 (mf/defc comments-layer
   [{:keys [vbox vport zoom file-id page-id drawing] :as props}]
-  (let [pos-x       (* (- (:x vbox)) zoom)
-        pos-y       (* (- (:y vbox)) zoom)
+  (let [pos-x                 (* (- (:x vbox)) zoom)
+        pos-y                 (* (- (:y vbox)) zoom)
 
-        profile     (mf/deref refs/profile)
-        users       (mf/deref refs/current-file-comments-users)
-        local       (mf/deref refs/comments-local)
-        threads-map (mf/deref refs/threads-ref)
+        profile               (mf/deref refs/profile)
+        users                 (mf/deref refs/current-file-comments-users)
+        local                 (mf/deref refs/comments-local)
+        threads-position-ref  (l/derived (l/in [:workspace-data :pages-index page-id :options :comment-threads-position]) st/state)
+        threads-position-map  (mf/deref threads-position-ref)
+        threads-map           (mf/deref refs/threads-ref)
 
-        threads     (->> (vals threads-map)
-                         (filter #(= (:page-id %) page-id))
-                         (dcm/apply-filters local profile))
+        update-thread-position (fn update-thread-position [thread]
+                                 (if (contains? threads-position-map (:id thread))
+                                   (-> thread
+                                       (assoc :position (get-in threads-position-map [(:id thread) :position]))
+                                       (assoc :frame-id (get-in threads-position-map [(:id thread) :frame-id])))
+                                   thread))
 
-        on-bubble-click
-        (fn [{:keys [id] :as thread}]
-          (if (= (:open local) id)
-            (st/emit! (dcm/close-thread))
-            (st/emit! (dcm/open-thread thread))))
+        threads               (->> (vals threads-map)
+                                   (filter #(= (:page-id %) page-id))
+                                   (mapv update-thread-position)
+                                   (dcm/apply-filters local profile))
 
         on-draft-cancel
         (mf/use-callback
@@ -41,7 +46,7 @@
         on-draft-submit
         (mf/use-callback
          (fn [draft]
-           (st/emit! (dcm/create-thread draft))))]
+           (st/emit! (dcm/create-thread-on-workspace draft))))]
 
     (mf/use-effect
      (mf/deps file-id)
@@ -58,13 +63,12 @@
        (for [item threads]
          [:& cmt/thread-bubble {:thread item
                                 :zoom zoom
-                                :on-click on-bubble-click
                                 :open? (= (:id item) (:open local))
                                 :key (:seqn item)}])
 
        (when-let [id (:open local)]
          (when-let [thread (get threads-map id)]
-           [:& cmt/thread-comments {:thread thread
+           [:& cmt/thread-comments {:thread (update-thread-position thread)
                                     :users users
                                     :zoom zoom}]))
 
@@ -73,5 +77,3 @@
                                :on-cancel on-draft-cancel
                                :on-submit on-draft-submit
                                :zoom zoom}])]]]))
-
-