diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs
index 0229db75a..61dc3dee7 100644
--- a/frontend/src/app/main/data/comments.cljs
+++ b/frontend/src/app/main/data/comments.cljs
@@ -56,27 +56,30 @@
 (declare refresh-comment-thread)
 
 (defn created-thread-on-workspace
-  [{:keys [id comment page-id] :as thread}]
-  (ptk/reify ::created-thread-on-workspace
-    ptk/UpdateEvent
-    (update [_ state]
-      (let [position (select-keys thread [:position :frame-id])]
-        (-> state
-            (update :comment-threads assoc id (dissoc thread :comment))
-            (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position)
-            (update :comments-local assoc :open id)
-            (update :comments-local assoc :options nil)
-            (update :comments-local dissoc :draft)
-            (update :workspace-drawing dissoc :comment)
-            (update-in [:comments id] assoc (:id comment) comment))))
+  ([params]
+   (created-thread-on-workspace params true))
+  ([{:keys [id comment page-id] :as thread} open?]
+   (ptk/reify ::created-thread-on-workspace
+     ptk/UpdateEvent
+     (update [_ state]
+       (let [position (select-keys thread [:position :frame-id])]
+         (-> state
+             (update :comment-threads assoc id (dissoc thread :comment))
+             (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position)
+             (cond-> open?
+               (update :comments-local assoc :open id))
+             (update :comments-local assoc :options nil)
+             (update :comments-local dissoc :draft)
+             (update :workspace-drawing dissoc :comment)
+             (update-in [:comments id] assoc (:id comment) comment))))
 
-    ptk/WatchEvent
-    (watch [_ _ _]
-      (rx/of (ptk/data-event ::ev/event
-                             {::ev/name "create-comment-thread"
-                              ::ev/origin "workspace"
-                              :id id
-                              :content-size (count (:content comment))})))))
+     ptk/WatchEvent
+     (watch [_ _ _]
+       (rx/of (ptk/data-event ::ev/event
+                              {::ev/name "create-comment-thread"
+                               ::ev/origin "workspace"
+                               :id id
+                               :content-size (count (:content comment))}))))))
 
 
 
@@ -89,24 +92,27 @@
    [:content :string]])
 
 (defn create-thread-on-workspace
-  [params]
-  (dm/assert! (sm/check! schema:create-thread-on-workspace params))
+  ([params]
+   (create-thread-on-workspace params identity true))
+  ([params on-thread-created open?]
+   (dm/assert! (sm/check! schema:create-thread-on-workspace 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 (ctst/get-frame-id-by-position objects (:position params))
-            params (assoc params :frame-id frame-id)]
-        (->> (rp/cmd! :create-comment-thread params)
-             (rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)}))
-             (rx/map created-thread-on-workspace)
-             (rx/catch (fn [{:keys [type code] :as cause}]
-                         (if (and (= type :restriction)
-                                  (= code :max-quote-reached))
-                           (rx/throw cause)
-                           (rx/throw {:type :comment-error})))))))))
+   (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 (ctst/get-frame-id-by-position objects (:position params))
+             params (assoc params :frame-id frame-id)]
+         (->> (rp/cmd! :create-comment-thread params)
+              (rx/mapcat #(rp/cmd! :get-comment-thread {:file-id (:file-id %) :id (:id %)}))
+              (rx/tap on-thread-created)
+              (rx/map #(created-thread-on-workspace % open?))
+              (rx/catch (fn [{:keys [type code] :as cause}]
+                          (if (and (= type :restriction)
+                                   (= code :max-quote-reached))
+                            (rx/throw cause)
+                            (rx/throw {:type :comment-error}))))))))))
 
 (defn created-thread-on-viewer
   [{:keys [id comment page-id] :as thread}]
@@ -257,29 +263,31 @@
              (rx/map #(retrieve-comment-threads file-id)))))))
 
 (defn delete-comment-thread-on-workspace
-  [{:keys [id] :as thread}]
-  (dm/assert!
-   "expected valid comment thread"
-   (check-comment-thread! thread))
-  (ptk/reify ::delete-comment-thread-on-workspace
-    ptk/UpdateEvent
-    (update [_ state]
-      (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))))
+  ([params]
+   (delete-comment-thread-on-workspace params identity))
+  ([{:keys [id] :as thread} on-delete]
+   (dm/assert! (uuid? id))
+   
+   (ptk/reify ::delete-comment-thread-on-workspace
+     ptk/UpdateEvent
+     (update [_ state]
+       (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 [_ _ _]
-      (rx/concat
-       (->> (rp/cmd! :delete-comment-thread {:id id})
-            (rx/catch #(rx/throw {:type :comment-error}))
-            (rx/ignore))
-       (rx/of (ptk/data-event ::ev/event
-                              {::ev/name "delete-comment-thread"
-                               ::ev/origin "workspace"
-                               :id id}))))))
+     ptk/WatchEvent
+     (watch [_ _ _]
+       (rx/concat
+        (->> (rp/cmd! :delete-comment-thread {:id id})
+             (rx/catch #(rx/throw {:type :comment-error}))
+             (rx/tap on-delete)
+             (rx/ignore))
+        (rx/of (ptk/data-event ::ev/event
+                               {::ev/name "delete-comment-thread"
+                                ::ev/origin "workspace"
+                                :id id})))))))
 
 (defn delete-comment-thread-on-viewer
   [{:keys [id] :as thread}]
diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs
index a7f52d164..d53a53d8e 100644
--- a/frontend/src/app/plugins/api.cljs
+++ b/frontend/src/app/plugins/api.cljs
@@ -368,7 +368,33 @@
   (openPage
     [_ page]
     (let [id (obj/get page "$id")]
-      (st/emit! (dw/go-to-page id)))))
+      (st/emit! (dw/go-to-page id))))
+
+  (alignHorizontal
+    [_ _shapes _direction]
+    ;; TODO
+    )
+
+  (alignVertical
+    [_ _shapes _direction]
+    ;; TODO
+    )
+
+  (distributeHorizontal
+    [_ _shapes]
+    ;; TODO
+    )
+
+  (distributeVertical
+    [_ _shapes]
+    ;; TODO
+    )
+
+  (flatten
+    [_ _shapes]
+    ;; TODO
+    )
+  )
 
 (defn create-context
   [plugin-id]
diff --git a/frontend/src/app/plugins/comments.cljs b/frontend/src/app/plugins/comments.cljs
new file mode 100644
index 000000000..0e99af204
--- /dev/null
+++ b/frontend/src/app/plugins/comments.cljs
@@ -0,0 +1,164 @@
+;; 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) KALEIDOS INC
+
+(ns app.plugins.comments
+  (:require
+   [app.common.geom.point :as gpt]
+   [app.common.record :as crc]
+   [app.common.spec :as us]
+   [app.main.data.comments :as dc]
+   [app.main.data.workspace.comments :as dwc]
+   [app.main.repo :as rp]
+   [app.main.store :as st]
+   [app.plugins.format :as format]
+   [app.plugins.parser :as parser]
+   [app.plugins.register :as r]
+   [app.plugins.shape :as shape]
+   [app.plugins.user :as user]
+   [app.plugins.utils :as u]
+   [beicon.v2.core :as rx]
+   [promesa.core :as p]))
+
+(deftype CommentProxy [$plugin $file $page $thread $id]
+  Object
+  (remove [_]
+    (p/create
+     (fn [resolve reject]
+       (->> (rp/cmd! :delete-comment {:id $id})
+            (rx/tap #(st/emit! (dc/retrieve-comment-threads $file)))
+            (rx/subs! #(resolve) reject))))))
+
+(defn comment-proxy? [p]
+  (instance? CommentProxy p))
+
+(defn comment-proxy
+  [plugin-id file-id page-id thread-id users data]
+  (let [data* (atom data)]
+    (crc/add-properties!
+     (CommentProxy. plugin-id file-id page-id thread-id (:id data))
+     {:name "$plugin" :enumerable false :get (constantly plugin-id)}
+     {:name "$file" :enumerable false :get (constantly file-id)}
+     {:name "$page" :enumerable false :get (constantly page-id)}
+     {:name "$thread" :enumerable false :get (constantly thread-id)}
+     {:name "$id" :enumerable false :get (constantly (:id data))}
+
+     {:name "user" :get (fn [_] (user/user-proxy plugin-id (get users (:owner-id data))))}
+     {:name "date" :get (fn [_] (:created-at data))}
+
+     {:name "content"
+      :get (fn [_] (:content @data*))
+      :set
+      (fn [_ content]
+        (let [profile (:profile @st/state)]
+          (cond
+            (or (not (string? content)) (empty? content))
+            (u/display-not-valid :content "Not valid")
+            
+            (not= (:id profile) (:owner-id data))
+            (u/display-not-valid :content "Cannot change content from another user's comments")
+
+            (not (r/check-permission plugin-id "content:write"))
+            (u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
+
+            :else
+            (->> (rp/cmd! :update-comment {:id (:id data) :content content})
+                 (rx/tap #(st/emit! (dc/retrieve-comment-threads file-id)))
+                 (rx/subs! #(swap! data* assoc :content content))))))})))
+
+(deftype CommentThreadProxy [$plugin $file $page $users $id owner]
+  Object
+  (findComments
+    [_]
+    (p/create
+     (fn [resolve reject]
+       (->> (rp/cmd! :get-comments {:thread-id $id})
+            (rx/subs!
+             (fn [comments]
+               (resolve
+                (format/format-array
+                 #(comment-proxy $plugin $file $page $id $users %) comments)))
+             reject)))))
+
+  (reply
+    [_ content]
+    (cond
+      (not (r/check-permission $plugin "content:write"))
+      (u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
+
+      (or (not (string? content)) (empty? content))
+      (u/display-not-valid :content "Not valid")
+
+      :else
+      (p/create
+       (fn [resolve reject]
+         (->> (rp/cmd! :create-comment {:thread-id $id :content content})
+              (rx/subs! #(resolve (comment-proxy $plugin $file $page $id $users %)) reject))))))
+
+  (remove [_]
+    (let [profile (:profile @st/state)]
+      (cond
+        (not (r/check-permission $plugin "content:write"))
+        (u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission")
+
+        (not= (:id profile) owner)
+        (u/display-not-valid :content "Cannot change content from another user's comments")
+
+        :else
+        (p/create
+         (fn [resolve]
+           (p/create
+            (st/emit! (dc/delete-comment-thread-on-workspace {:id $id} #(resolve))))))))))
+
+(defn comment-thread-proxy? [p]
+  (instance? CommentThreadProxy p))
+
+(defn comment-thread-proxy
+  [plugin-id file-id page-id users data]
+  (let [data* (atom data)]
+    (crc/add-properties!
+     (CommentThreadProxy. plugin-id file-id page-id users (:id data) (:owner-id data))
+     {:name "$plugin" :enumerable false :get (constantly plugin-id)}
+     {:name "$file" :enumerable false :get (constantly file-id)}
+     {:name "$page" :enumerable false :get (constantly page-id)}
+     {:name "$id" :enumerable false :get (constantly (:id data))}
+     {:name "$users" :enumerable false :get (constantly users)}
+     {:name "page" :enumerable false :get (fn [_] (u/locate-page file-id page-id))}
+
+     {:name "seqNumber" :get (fn [_] (:seqn data))}
+     {:name "owner" :get (fn [_] (user/user-proxy plugin-id (get users (:owner-id data))))}
+     {:name "board" :get (fn [_] (shape/shape-proxy plugin-id file-id page-id (:frame-id data)))}
+     
+     {:name "position"
+      :get (fn [_] (format/format-point (:position @data*)))
+      :set
+      (fn [_ position]
+        (let [position (parser/parse-point position)]
+          (cond
+            (or (not (us/safe-number? (:x position))) (not (us/safe-number? (:y position))))
+            (u/display-not-valid :position "Not valid point")
+            
+            (not (r/check-permission plugin-id "content:write"))
+            (u/display-not-valid :content "Plugin doesn't have 'content:write' permission")
+
+            :else
+            (do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)]))
+                (swap! data* assoc :position (gpt/point position))))))}
+
+     {:name "resolved"
+      :get (fn [_] (:is-resolved @data*))
+      :set
+      (fn [_ is-resolved]
+        (cond
+          (not (boolean? is-resolved))
+          (u/display-not-valid :resolved "Not a boolean type")
+          
+          (not (r/check-permission plugin-id "content:write"))
+          (u/display-not-valid :resolved "Plugin doesn't have 'content:write' permission")
+
+          :else
+          (do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved)))
+              (swap! data* assoc :is-resolved is-resolved))))})))
+
diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs
index c62a2cece..defb243d4 100644
--- a/frontend/src/app/plugins/page.cljs
+++ b/frontend/src/app/plugins/page.cljs
@@ -10,13 +10,17 @@
    [app.common.data :as d]
    [app.common.data.macros :as dm]
    [app.common.files.helpers :as cfh]
+   [app.common.geom.point :as gpt]
    [app.common.record :as crc]
    [app.common.spec :as us]
    [app.common.uuid :as uuid]
+   [app.main.data.comments :as dc]
    [app.main.data.workspace :as dw]
    [app.main.data.workspace.guides :as dwgu]
    [app.main.data.workspace.interactions :as dwi]
+   [app.main.repo :as rp]
    [app.main.store :as st]
+   [app.plugins.comments :as pc]
    [app.plugins.format :as format]
    [app.plugins.parser :as parser]
    [app.plugins.register :as r]
@@ -24,7 +28,9 @@
    [app.plugins.shape :as shape]
    [app.plugins.utils :as u]
    [app.util.object :as obj]
-   [cuerdas.core :as str]))
+   [beicon.v2.core :as rx]
+   [cuerdas.core :as str]
+   [promesa.core :as p]))
 
 (deftype FlowProxy [$plugin $file $page $id]
   Object
@@ -78,8 +84,10 @@
       (u/display-not-valid :getShapeById shape-id)
 
       :else
-      (let [shape-id (uuid/uuid shape-id)]
-        (shape/shape-proxy $plugin $file $id shape-id))))
+      (let [shape-id (uuid/uuid shape-id)
+            shape (u/locate-shape $file $id shape-id)]
+        (when (some? shape)
+          (shape/shape-proxy $plugin $file $id shape-id)))))
 
   (getRoot
     [_]
@@ -215,7 +223,7 @@
       (st/emit! (dwi/remove-flow $id (obj/get flow "$id")))))
 
   (addRulerGuide
-    [self orientation value board]
+    [_ orientation value board]
     (let [shape (u/proxy->shape board)]
       (cond
         (not (us/safe-number? value))
@@ -224,14 +232,14 @@
         (not (contains? #{"vertical" "horizontal"} orientation))
         (u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'")
 
-        (or (not (shape/shape-proxy? shape))
+        (or (not (shape/shape-proxy? board))
             (not (cfh/frame-shape? shape)))
         (u/display-not-valid :addRulerGuide "The shape is not a board")
 
         (not (r/check-permission $plugin "content:write"))
         (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission")
 
-        :ellse
+        :else
         (let [id (uuid/next)]
           (st/emit!
            (dwgu/update-guides
@@ -253,7 +261,82 @@
 
       :else
       (let [guide (u/proxy->ruler-guide value)]
-        (st/emit! (dwgu/remove-guide guide))))))
+        (st/emit! (dwgu/remove-guide guide)))))
+
+  (addCommentThread
+    [_ content position board]
+    (let [shape (when board (u/proxy->shape board))]
+      (cond
+        (or (not (string? content)) (empty? content))
+        (u/display-not-valid :addCommentThread "Content not valid")
+
+        (or (not (us/safe-number? (:x position))) (not (us/safe-number? (:y position))))
+        (u/display-not-valid :addCommentThread "Position not valid")
+
+        (and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))
+        (u/display-not-valid :addCommentThread "Board not valid")
+
+        (not (r/check-permission $plugin "content:write"))
+        (u/display-not-valid :addCommentThread "Plugin doesn't have 'content:write' permission")
+
+        :else
+        (p/create
+         (fn [resolve]
+           (st/emit!
+            (dc/create-thread-on-workspace
+             {:file-id $file
+              :page-id $id
+              :position (gpt/point (parser/parse-point position))
+              :content content}
+
+             (fn [data]
+               (->> (rp/cmd! :get-team-users {:file-id $file})
+                    (rx/subs!
+                     (fn [users]
+                       (let [users (d/index-by :id users)]
+                         (resolve (pc/comment-thread-proxy $plugin $file $id users data)))))))
+             false)))))))
+
+  (removeCommentThread
+    [_ thread]
+    (cond
+      (not (pc/comment-thread-proxy? thread))
+      (u/display-not-valid :removeCommentThread "Comment thread not valid")
+
+      (not (r/check-permission $plugin "content:write"))
+      (u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission")
+
+      :else
+      (p/create
+       (fn [resolve]
+         (let [thread-id (obj/get thread "$id")]
+           (p/create
+            (st/emit! (dc/delete-comment-thread-on-workspace {:id thread-id} #(resolve)))))))))
+
+  (findCommentThreads
+    [_ criteria]
+    (let [only-yours    (boolean (obj/get criteria "onlyYours" false))
+          show-resolved (boolean (obj/get criteria "showResolved" true))
+          user-id      (-> @st/state :profile :id)]
+      (p/create
+       (fn [resolve reject]
+         (->> (rx/zip (rp/cmd! :get-team-users {:file-id $file})
+                      (rp/cmd! :get-comment-threads {:file-id $file}))
+              (rx/take 1)
+              (rx/subs!
+               (fn [[users comments]]
+                 (let [users (d/index-by :id users)]
+                   (let [comments
+                         (cond->> comments
+                           (not show-resolved)
+                           (filter (comp not :is-resolved))
+
+                           only-yours
+                           (filter #(contains? (:participants %) user-id)))]
+                     (resolve
+                      (format/format-array
+                       #(pc/comment-thread-proxy $plugin $file $id users %) comments)))))
+               reject)))))))
 
 (crc/define-properties!
   PageProxy
diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs
index 59685374d..d0e8addbf 100644
--- a/frontend/src/app/plugins/ruler_guides.cljs
+++ b/frontend/src/app/plugins/ruler_guides.cljs
@@ -17,8 +17,8 @@
    [app.plugins.utils :as u]
    [app.util.object :as obj]))
 
-(def shape-proxy)
-(def shape-proxy?)
+(def shape-proxy identity)
+(def shape-proxy? identity)
 
 (deftype RulerGuideProxy [$plugin $file $page $id]
   Object
diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs
index 0616288b3..891888688 100644
--- a/frontend/src/app/plugins/shape.cljs
+++ b/frontend/src/app/plugins/shape.cljs
@@ -591,7 +591,7 @@
         (not (r/check-permission $plugin "content:write"))
         (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission")
 
-        :ellse
+        :else
         (let [id        (uuid/next)
               axis      (parser/orientation->axis orientation)
               objects   (u/locate-objects $file $page)
diff --git a/frontend/src/app/plugins/user.cljs b/frontend/src/app/plugins/user.cljs
index 220a5e08c..20af5481e 100644
--- a/frontend/src/app/plugins/user.cljs
+++ b/frontend/src/app/plugins/user.cljs
@@ -12,13 +12,13 @@
    [app.plugins.utils :as u]
    [app.util.object :as obj]))
 
-(deftype CurrentUserProxy [$plugin $session])
-(deftype ActiveUserProxy [$plugin $session])
+(deftype CurrentUserProxy [$plugin])
+(deftype ActiveUserProxy [$plugin])
+(deftype UserProxy [$plugin])
 
-(defn add-user-properties
-  [user-proxy]
-  (let [plugin-id (obj/get user-proxy "$plugin")
-        session-id (obj/get user-proxy "$session")]
+(defn- add-session-properties
+  [user-proxy session-id]
+  (let [plugin-id (obj/get user-proxy "$plugin")]
     (crc/add-properties!
      user-proxy
      {:name "$plugin" :enumerable false :get (constantly plugin-id)}
@@ -39,21 +39,43 @@
      {:name "sessionId"
       :get (fn [_] (str session-id))})))
 
+
 (defn current-user-proxy? [p]
   (instance? CurrentUserProxy p))
 
 (defn current-user-proxy
   [plugin-id session-id]
-  (-> (CurrentUserProxy. plugin-id session-id)
-      (add-user-properties)))
+  (-> (CurrentUserProxy. plugin-id)
+      (add-session-properties session-id)))
 
 (defn active-user-proxy? [p]
   (instance? ActiveUserProxy p))
 
 (defn active-user-proxy
   [plugin-id session-id]
-  (-> (ActiveUserProxy. plugin-id session-id)
-      (add-user-properties)
+  (-> (ActiveUserProxy. plugin-id)
+      (add-session-properties session-id)
       (crc/add-properties!
        {:name "position" :get (fn [_] (-> (u/locate-presence session-id) :point format/format-point))}
        {:name "zoom" :get (fn [_] (-> (u/locate-presence session-id) :zoom))})))
+
+(defn- add-user-properties
+  [user-proxy data]
+  (let [plugin-id (obj/get user-proxy "$plugin")]
+    (crc/add-properties!
+     user-proxy
+     {:name "$plugin" :enumerable false :get (constantly plugin-id)}
+
+     {:name "id"
+      :get (fn [_] (-> data :id str))}
+
+     {:name "name"
+      :get (fn [_] (-> data :fullname))}
+
+     {:name "avatarUrl"
+      :get (fn [_] (cfg/resolve-profile-photo-url data))})))
+
+(defn user-proxy
+  [plugin-id data]
+  (-> (UserProxy. plugin-id)
+      (add-user-properties data)))
diff --git a/frontend/src/app/plugins/viewport.cljs b/frontend/src/app/plugins/viewport.cljs
index 6973f33f7..8f2fb4c32 100644
--- a/frontend/src/app/plugins/viewport.cljs
+++ b/frontend/src/app/plugins/viewport.cljs
@@ -20,6 +20,14 @@
 
 (deftype ViewportProxy [$plugin]
   Object
+  (zoomReset [_]
+    ;;TODO
+    )
+
+  (zoomToFitAll [_]
+    ;;TODO
+    )
+
   (zoomIntoView [_ shapes]
     (let [ids
           (->> shapes