diff --git a/backend/src/app/rpc/mutations/files.clj b/backend/src/app/rpc/mutations/files.clj
index d6fb24c59..d90e453fb 100644
--- a/backend/src/app/rpc/mutations/files.clj
+++ b/backend/src/app/rpc/mutations/files.clj
@@ -54,11 +54,11 @@
          (db/insert! conn :file-profile-rel))))
 
 (defn create-file
-  [conn {:keys [id name project-id is-shared]
+  [conn {:keys [id name project-id is-shared data]
          :or {is-shared false}
          :as params}]
-  (let [id   (or id (uuid/next))
-        data (cp/make-file-data id)
+  (let [id   (or id (:id data) (uuid/next))
+        data (or data (cp/make-file-data id))
         file (db/insert! conn :file
                          {:id id
                           :project-id project-id
diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc
new file mode 100644
index 000000000..807d507c2
--- /dev/null
+++ b/common/src/app/common/file_builder.cljc
@@ -0,0 +1,159 @@
+;; 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.common.file-builder
+  "A version parsing helper."
+  (:require
+   [app.common.geom.shapes :as gsh]
+   [app.common.pages.changes :as ch]
+   [app.common.pages.init :as init]
+   [app.common.pages.spec :as spec]
+   [app.common.spec :as us]
+   [app.common.spec :as us]
+   [app.common.uuid :as uuid]))
+
+(def root-frame uuid/zero)
+
+;; This flag controls if we should execute spec validation after every commit
+(def verify-on-commit? true)
+
+(defn- commit-change [file change]
+  (when verify-on-commit?
+    (us/assert ::spec/change change))
+  (-> file
+      (update :changes conj change)
+      (update :data ch/process-changes [change] verify-on-commit?)))
+
+(defn- lookup-objects
+  ([file]
+   (lookup-objects file (:current-page-id file)))
+
+  ([file page-id]
+   (get-in file [:data :pages-index page-id :objects])))
+
+(defn- lookup-shape [file shape-id]
+  (-> (lookup-objects file)
+      (get shape-id)))
+
+(defn- commit-shape [file obj]
+  (let [page-id (:current-page-id file)
+        frame-id (:current-frame-id file)
+        parent-id (-> file :parent-stack peek)]
+    (-> file
+        (commit-change
+         {:type :add-obj
+          :id (:id obj)
+          :page-id page-id
+          :frame-id frame-id
+          :parent-id parent-id
+          :obj obj}))))
+
+;; PUBLIC API
+
+(defn create-file
+  ([name]
+   (let [id (uuid/next)]
+     {:id id
+      :name name
+      :data (-> init/empty-file-data
+                (assoc :id id))
+
+      ;; We keep the changes so we can send them to the backend
+      :changes []})))
+
+(defn add-page
+  [file name]
+  (let [page-id (uuid/next)]
+    (-> file
+        (commit-change
+         {:type :add-page
+          :id page-id
+          :name name
+          :page (-> init/empty-page-data
+                    (assoc :name name))})
+
+        ;; Current page being edited
+        (assoc :current-page-id page-id)
+
+        ;; Current frame-id
+        (assoc :current-frame-id root-frame)
+
+        ;; Current parent stack we'll be nesting
+        (assoc :parent-stack [root-frame]))))
+
+(defn add-artboard [file data]
+  (let [obj (-> (init/make-minimal-shape :frame)
+                (merge data))]
+    (-> file
+        (commit-shape obj)
+        (assoc :current-frame-id (:id obj))
+        (update :parent-stack conj (:id obj)))))
+
+(defn close-artboard [file]
+  (-> file
+      (assoc :current-frame-id root-frame)
+      (update :parent-stack pop)))
+
+(defn add-group [file data]
+  (let [frame-id (:current-frame-id file)
+        selrect init/empty-selrect
+        name (:name data)
+        obj (-> (init/make-minimal-group frame-id selrect name)
+                (merge data))]
+    (-> file
+        (commit-shape obj)
+        (update :parent-stack conj (:id obj)))))
+
+(defn close-group [file]
+  (let [group-id (-> file :parent-stack peek)
+        group    (lookup-shape file group-id)
+        shapes   (->> group :shapes (mapv #(lookup-shape file %)))
+        selrect  (gsh/selection-rect shapes)
+        points   (gsh/rect->points selrect)]
+
+    (-> file
+        (commit-change
+         {:type :mod-obj
+          :page-id (:current-page-id file)
+          :id group-id
+          :operations
+          [{:type :set :attr :selrect :val selrect}
+           {:type :set :attr :points  :val points}]})
+        (update :parent-stack pop))))
+
+(defn create-shape [file type data]
+  (let [frame-id (:current-frame-id file)
+        frame (when-not (= frame-id root-frame)
+                (lookup-shape file frame-id))
+        obj (-> (init/make-minimal-shape type)
+                (merge data)
+                (cond-> frame
+                  (gsh/translate-from-frame frame)))]
+    (commit-shape file obj)))
+
+(defn create-rect [file data]
+  (create-shape file :rect data))
+
+(defn create-circle [file data]
+  (create-shape file :circle data))
+
+(defn create-path [file data]
+  (create-shape file :path data))
+
+(defn create-text [file data]
+  (create-shape file :text data))
+
+(defn create-image [file data]
+  (create-shape file :image data))
+
+(defn close-page [file]
+  (-> file
+      (dissoc :current-page-id)
+      (dissoc :parent-stack)))
+
+(defn generate-changes
+  [file]
+  (:changes file))
diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc
index 1c0a83482..fc2513f4a 100644
--- a/common/src/app/common/geom/matrix.cljc
+++ b/common/src/app/common/geom/matrix.cljc
@@ -8,6 +8,7 @@
   (:require
    #?(:cljs [cljs.pprint :as pp]
       :clj  [clojure.pprint :as pp])
+   [app.common.data :as d]
    [app.common.geom.point :as gpt]
    [app.common.math :as mth]))
 
@@ -25,6 +26,15 @@
   ([a b c d e f]
    (Matrix. a b c d e f)))
 
+(def number-regex #"[+-]?\d*(\.\d+)?(e[+-]?\d+)?")
+
+(defn str->matrix
+  [matrix-str]
+  (let [params (->> (re-seq number-regex matrix-str)
+                    (filter #(-> % first empty? not))
+                    (map (comp d/parse-double first)))]
+    (apply matrix params)))
+
 (defn multiply
   ([{m1a :a m1b :b m1c :c m1d :d m1e :e m1f :f}
     {m2a :a m2b :b m2c :c m2d :d m2e :e m2f :f}]
diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc
index 96e489157..75fc70661 100644
--- a/common/src/app/common/geom/shapes.cljc
+++ b/common/src/app/common/geom/shapes.cljc
@@ -100,6 +100,10 @@
   [shape {:keys [x y]}]
   (gtr/move shape (gpt/negate (gpt/point x y)))  )
 
+(defn translate-from-frame
+  [shape {:keys [x y]}]
+  (gtr/move shape (gpt/point x y))  )
+
 ;; --- Helpers
 
 (defn fully-contained?
diff --git a/common/src/app/common/pages.cljc b/common/src/app/common/pages.cljc
index 32bd26084..0e69c9d53 100644
--- a/common/src/app/common/pages.cljc
+++ b/common/src/app/common/pages.cljc
@@ -84,6 +84,7 @@
 (d/export init/make-file-data)
 (d/export init/make-minimal-shape)
 (d/export init/make-minimal-group)
+(d/export init/empty-file-data)
 
 ;; Specs
 
diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc
index 08d222f34..ca42025f4 100644
--- a/common/src/app/common/pages/init.cljc
+++ b/common/src/app/common/pages/init.cljc
@@ -85,6 +85,12 @@
 
    {:type :svg-raw}])
 
+(def empty-selrect
+  {:x  0    :y  0
+   :x1 0    :y1 0
+   :x2 1    :y2 1
+   :width 1 :height 1})
+
 (defn make-minimal-shape
   [type]
   (let [type (cond (= type :curve) :path
@@ -126,10 +132,11 @@
    :height (:height selection-rect)})
 
 (defn make-file-data
-  ([file-id] (make-file-data file-id(uuid/next)))
+  ([file-id]
+   (make-file-data file-id (uuid/next)))
+
   ([file-id page-id]
-   (let [
-         pd (assoc empty-page-data
+   (let [pd (assoc empty-page-data
                    :id page-id
                    :name "Page-1")]
      (-> empty-file-data
diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js
index 2c2a9f45b..e05f35853 100644
--- a/common/src/app/common/uuid_impl.js
+++ b/common/src/app/common/uuid_impl.js
@@ -16,7 +16,8 @@ goog.scope(function() {
   const self = app.common.uuid_impl;
 
   const fill = (() => {
-    if (typeof global.crypto !== "undefined") {
+    if (typeof global.crypto !== "undefined" &&
+        typeof global.crypto.getRandomValues !== "undefined") {
       return (buf) => {
         global.crypto.getRandomValues(buf);
         return buf;
diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss
index f8423ce48..780dd8794 100644
--- a/frontend/resources/styles/main/partials/dashboard.scss
+++ b/frontend/resources/styles/main/partials/dashboard.scss
@@ -165,3 +165,30 @@
     }
   }
 }
+
+.import-file-btn {
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  height: 2rem;
+  justify-content: center;
+  overflow: hidden;
+  padding: 4px;
+  width: 2rem;
+
+  background: none;
+  border: 1px solid $color-gray-20;
+  border-radius: 2px;
+  cursor: pointer;
+  transition: all 0.4s;
+  margin-left: 1rem;
+
+  &:hover {
+    background: $color-primary;
+  }
+
+  svg {
+    width: 16px;
+    height: 16px;
+  }
+}
diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn
index 82c7116a6..1d9527552 100644
--- a/frontend/shadow-cljs.edn
+++ b/frontend/shadow-cljs.edn
@@ -4,6 +4,7 @@
  :jvm-opts ["-Xmx700m" "-Xms100m" "-XX:+UseSerialGC" "-XX:-OmitStackTraceInFastThrow"]
  :dev-http {8888 "classpath:public"}
 
+
  :builds
  {:main
   {:target :browser
@@ -35,6 +36,27 @@
      :anon-fn-naming-policy :off
      :source-map-detail-level :all}}}
 
+  :lib-penpot
+  {:target :esm
+   :output-dir "resources/public/libs"
+
+   :modules
+   {:penpot {:exports {:renderPage app.libs.render/render-page-export
+                       :createFile app.libs.file-builder/create-file-export}}}
+
+   :compiler-options
+   {:output-feature-set :es8
+    :output-wrapper false
+    :warnings {:fn-deprecated false}}
+
+   :release
+   {:compiler-options
+    {:fn-invoke-direct true
+     :source-map true
+     :elide-asserts true
+     :anon-fn-naming-policy :off
+     :source-map-detail-level :all}}}
+
   :test
   {:target :node-test
    :output-to "target/tests.js"
@@ -45,4 +67,3 @@
    {:output-feature-set :es8
     :output-wrapper false
     :warnings {:fn-deprecated false}}}}}
-
diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs
new file mode 100644
index 000000000..4f1c6a207
--- /dev/null
+++ b/frontend/src/app/libs/file_builder.cljs
@@ -0,0 +1,23 @@
+;; 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.libs.file-builder
+  (:require
+   [app.common.data :as d]
+   [app.common.file-builder :as fb]))
+
+(deftype File [^:mutable file]
+  Object
+  (addPage [self name]
+    (set! file (fb/add-page file name))
+    (str (:current-page-id file))))
+
+
+(defn create-file-export [^string name]
+  (File. (fb/create-file name)))
+
+(defn exports []
+  #js { :createFile    create-file-export })
diff --git a/frontend/src/app/libs/render.cljs b/frontend/src/app/libs/render.cljs
new file mode 100644
index 000000000..73006a840
--- /dev/null
+++ b/frontend/src/app/libs/render.cljs
@@ -0,0 +1,28 @@
+;; 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.libs.render
+  (:require
+   [app.common.uuid :as uuid]
+   [app.main.render :as r]
+   [beicon.core :as rx]
+   [promesa.core :as p]))
+
+(defn render-page-export
+  [file ^string page-id]
+
+  ;; Better to expose the api as a promise to be consumed from JS
+  (let [page-id (uuid/uuid page-id)
+        file-data (.-file file)
+        data (get-in file-data [:data :pages-index page-id])]
+    (p/create
+     (fn [resolve reject]
+       (->> (r/render-page data)
+            (rx/take 1)
+            (rx/subs resolve reject))) )))
+
+(defn exports []
+  #js {:renderPage render-page-export})
diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs
new file mode 100644
index 000000000..5ba1ef0d3
--- /dev/null
+++ b/frontend/src/app/main/render.cljs
@@ -0,0 +1,74 @@
+;; 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.render
+  (:require
+   ["react-dom/server" :as rds]
+   [app.config :as cfg]
+   [app.main.exports :as exports]
+   [app.main.exports :as svg]
+   [app.main.fonts :as fonts]
+   [app.util.http :as http]
+   [beicon.core :as rx]
+   [clojure.set :as set]
+   [rumext.alpha :as mf]))
+
+(defn- text? [{type :type}]
+  (= type :text))
+
+(defn- get-image-data [shape]
+  (cond
+    (= :image (:type shape))
+    [(:metadata shape)]
+
+    (some? (:fill-image shape))
+    [(:fill-image shape)]
+
+    :else
+    []))
+
+(defn populate-images-cache
+  ([data]
+   (populate-images-cache data nil))
+
+  ([data {:keys [resolve-media?] :or {resolve-media? false}}]
+   (let [images (->> (:objects data)
+                     (vals)
+                     (mapcat get-image-data))]
+     (->> (rx/from images)
+          (rx/map #(cfg/resolve-file-media %))
+          (rx/flat-map http/fetch-data-uri)))))
+
+(defn populate-fonts-cache [data]
+  (let [texts (->> (:objects data)
+                   (vals)
+                   (filterv text?)
+                   (mapv :content)) ]
+
+    (->> (rx/from texts)
+         (rx/map fonts/get-content-fonts)
+         (rx/reduce set/union #{})
+         (rx/flat-map identity)
+         (rx/flat-map fonts/fetch-font-css)
+         (rx/flat-map fonts/extract-fontface-urls)
+         (rx/flat-map http/fetch-data-uri))))
+
+(defn render-page
+  [data]
+  (rx/concat
+   (->> (rx/merge
+         (populate-images-cache data)
+         (populate-fonts-cache data))
+        (rx/ignore))
+
+   (->> (rx/of data)
+        (rx/map
+         (fn [data]
+           (let [elem (mf/element exports/page-svg #js {:data data :embed? true})]
+             (rds/renderToStaticMarkup elem)))))))
+
+
+
diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs
index f24a8d412..6dcedd52a 100644
--- a/frontend/src/app/main/ui/dashboard/file_menu.cljs
+++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs
@@ -11,8 +11,9 @@
    [app.main.data.modal :as modal]
    [app.main.repo :as rp]
    [app.main.store :as st]
-   [app.main.ui.context :as ctx]
    [app.main.ui.components.context-menu :refer [context-menu]]
+   [app.main.ui.context :as ctx]
+   [app.main.worker :as uw]
    [app.util.dom :as dom]
    [app.util.i18n :as i18n :refer [tr]]
    [app.util.router :as rt]
@@ -150,7 +151,22 @@
                       :hint (tr "modals.remove-shared-confirm.hint")
                       :cancel-label :omit
                       :accept-label (tr "modals.remove-shared-confirm.accept")
-                      :on-accept del-shared})))]
+                      :on-accept del-shared})))
+
+        on-export-files
+        (fn [event]
+          (->> (uw/ask-many!
+                {:cmd :export-file
+                 :team-id current-team-id
+                 :files files})
+               (rx/subs
+                (fn [{:keys [type data] :as msg}]
+                  (case type
+                    :progress
+                    (prn "[Progress]" data)
+
+                    :finish
+                    (dom/save-as data "export" "application/zip" "Export package (*.zip)"))))))]
 
     (mf/use-effect
      (fn []
@@ -176,6 +192,7 @@
                       [[(tr "dashboard.duplicate-multi" file-count) on-duplicate]
                        (when (or (seq current-projects) (seq other-teams))
                          [(tr "dashboard.move-to-multi" file-count) nil sub-options])
+                       #_[(tr "dashboard.export-multi" file-count) on-export-files]
                        [:separator]
                        [(tr "labels.delete-multi-files" file-count) on-delete]]
 
@@ -187,6 +204,7 @@
                        (if (:is-shared file)
                          [(tr "dashboard.remove-shared") on-del-shared]
                          [(tr "dashboard.add-shared") on-add-shared])
+                       #_[(tr "dashboard.export-single") on-export-files]
                        [:separator]
                        [(tr "labels.delete") on-delete]])]
 
diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs
new file mode 100644
index 000000000..c30e48f03
--- /dev/null
+++ b/frontend/src/app/main/ui/dashboard/import.cljs
@@ -0,0 +1,57 @@
+;; 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.ui.dashboard.import
+  (:require
+   [app.main.ui.components.file-uploader :refer [file-uploader]]
+   [app.main.ui.icons :as i]
+   [app.main.worker :as uw]
+   [app.util.dom :as dom]
+   [app.util.logging :as log]
+   [beicon.core :as rx]
+   [rumext.alpha :as mf]))
+
+(log/set-level! :debug)
+
+(defn use-import-file
+  [project-id on-finish-import]
+  (mf/use-callback
+   (mf/deps project-id on-finish-import)
+   (fn [files]
+     (when files
+       (let [files (->> files (mapv dom/create-uri))]
+         (->> (uw/ask-many!
+               {:cmd :import-file
+                :project-id project-id
+                :files files})
+
+              (rx/subs
+               (fn [result]
+                 (log/debug :action "import-result" :result result))
+
+               (fn [err]
+                 (log/debug :action "import-error" :result err))
+
+               (fn []
+                 (log/debug :action "import-end")
+                 (when on-finish-import (on-finish-import))))))))))
+
+(mf/defc import-button
+  [{:keys [project-id on-finish-import]}]
+
+  (let [file-input (mf/use-ref nil)
+        on-file-selected (use-import-file project-id on-finish-import)]
+    [:form.import-file
+     [:button.import-file-btn {:type "button"
+                               :on-click #(dom/click (mf/ref-val file-input))} i/import]
+     [:& file-uploader {:accept "application/zip"
+                        :multi true
+                        :input-ref file-input
+                        :on-selected on-file-selected}]]))
+
+
+
+
diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs
index 342d377f0..7921624aa 100644
--- a/frontend/src/app/main/ui/dashboard/projects.cljs
+++ b/frontend/src/app/main/ui/dashboard/projects.cljs
@@ -21,7 +21,8 @@
    [app.util.router :as rt]
    [app.util.time :as dt]
    [okulary.core :as l]
-   [rumext.alpha :as mf]))
+   [rumext.alpha :as mf]
+   [app.main.ui.dashboard.import :refer [import-button]]))
 
 (mf/defc header
   {::mf/wrap [mf/memo]}
@@ -30,6 +31,7 @@
     [:header.dashboard-header
      [:div.dashboard-title
       [:h1 (tr "dashboard.projects-title")]]
+
      [:a.btn-secondary.btn-small {:on-click create}
       (tr "dashboard.new-project")]]))
 
@@ -96,7 +98,13 @@
          (fn []
            (let [mdata  {:on-success on-file-created}
                  params {:project-id (:id project)}]
-             (st/emit! (dd/create-file (with-meta params mdata))))))]
+             (st/emit! (dd/create-file (with-meta params mdata))))))
+
+        on-finish-import
+        (mf/use-callback
+         (fn []
+           (st/emit! (dd/fetch-recent-files)
+                     (dd/clear-selected-files))))]
 
     [:div.dashboard-project-row {:class (when first? "first")}
      [:div.project
@@ -130,6 +138,9 @@
                        (dt/timeago {:locale locale}))]
           [:span.recent-files-row-title-info (str ", " time)]))
 
+      #_[:& import-button {:project-id (:id project)
+                         :on-finish-import on-finish-import}]
+
       [:a.btn-secondary.btn-small
        {:on-click create-file}
        (tr "dashboard.new-file")]]
diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs
index 598ba3b48..e577e082b 100644
--- a/frontend/src/app/main/ui/icons.cljs
+++ b/frontend/src/app/main/ui/icons.cljs
@@ -5,6 +5,7 @@
 ;; Copyright (c) UXBOX Labs SL
 
 (ns app.main.ui.icons
+  (:refer-clojure :exclude [import])
   (:require-macros [app.main.ui.icons :refer [icon-xref]])
   (:require [rumext.alpha :as mf]))
 
@@ -53,6 +54,7 @@
 (def icon-set (icon-xref :icon-set))
 (def icon-verify (icon-xref :icon-verify))
 (def image (icon-xref :image))
+(def import (icon-xref :import))
 (def infocard (icon-xref :infocard))
 (def interaction (icon-xref :interaction))
 (def layers (icon-xref :layers))
@@ -60,9 +62,9 @@
 (def libraries (icon-xref :libraries))
 (def library (icon-xref :library))
 (def line (icon-xref :line))
+(def line-height (icon-xref :line-height))
 (def listing-enum (icon-xref :listing-enum))
 (def listing-thumbs (icon-xref :listing-thumbs))
-(def line-height (icon-xref :line-height))
 (def loader (icon-xref :loader))
 (def lock (icon-xref :lock))
 (def logo (icon-xref :uxbox-logo))
diff --git a/frontend/src/app/main/ui/render.cljs b/frontend/src/app/main/ui/render.cljs
index f7de7b862..c6853ecd2 100644
--- a/frontend/src/app/main/ui/render.cljs
+++ b/frontend/src/app/main/ui/render.cljs
@@ -78,7 +78,8 @@
             :height height
             :version "1.1"
             :xmlnsXlink "http://www.w3.org/1999/xlink"
-            :xmlns "http://www.w3.org/2000/svg"}
+            :xmlns "http://www.w3.org/2000/svg"
+            :xmlns:penpot "https://penpot.app/xmlns"}
       (case (:type object)
         :frame [:& frame-wrapper {:shape object :view-box vbox}]
         :group [:> shape-container {:shape object}
diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs
index b3cdecbf0..da19c2003 100644
--- a/frontend/src/app/main/ui/shapes/circle.cljs
+++ b/frontend/src/app/main/ui/shapes/circle.cljs
@@ -32,6 +32,5 @@
                         :ry ry
                         :transform transform}))]
 
-    [:& shape-custom-stroke {:shape shape
-                             :base-props props
-                             :elem-name "ellipse"}]))
+    [:& shape-custom-stroke {:shape shape}
+     [:> :ellipse props]]))
diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs
index 643f07485..1e19f219f 100644
--- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs
+++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs
@@ -6,10 +6,130 @@
 
 (ns app.main.ui.shapes.custom-stroke
   (:require
-   [rumext.alpha :as mf]
-   [app.common.uuid :as uuid]
+   [app.common.data :as d]
    [app.common.geom.shapes :as geom]
-   [app.util.object :as obj]))
+   [app.common.uuid :as uuid]
+   [app.main.ui.context :as muc]
+   [app.util.object :as obj]
+   [cuerdas.core :as str]
+   [rumext.alpha :as mf]))
+
+(defn add-props
+  [props new-props]
+  (-> props
+      (obj/merge (clj->js new-props))))
+
+(defn add-style
+  [props new-style]
+  (let [old-style (obj/get props "style")
+        style (obj/merge old-style (clj->js new-style))]
+    (-> props (obj/merge #js {:style style}))))
+
+(mf/defc inner-stroke-clip-path
+  [{:keys [shape render-id]}]
+  (let [clip-id (str "inner-stroke-" render-id)
+        shape-id (str "stroke-shape-" render-id)]
+    [:> "clipPath" #js {:id clip-id}
+     [:use {:href (str "#" shape-id)}]]))
+
+(mf/defc outer-stroke-mask
+  [{:keys [shape render-id]}]
+  (let [stroke-mask-id (str "outer-stroke-" render-id)
+        shape-id (str "stroke-shape-" render-id)
+        stroke-width (:stroke-width shape 0)]
+    [:mask {:id stroke-mask-id}
+     [:use {:href (str "#" shape-id)
+            :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}]
+
+     [:use {:href (str "#" shape-id)
+            :style #js {:fill "black"}}]]))
+
+(mf/defc stroke-defs
+  [{:keys [shape render-id]}]
+  (cond
+    (and (= :inner (:stroke-alignment shape :center))
+         (> (:stroke-width shape 0) 0))
+    [:& inner-stroke-clip-path {:shape shape
+                                :render-id render-id}]
+
+    (and (= :outer (:stroke-alignment shape :center))
+         (> (:stroke-width shape 0) 0))
+    [:& outer-stroke-mask {:shape shape
+                           :render-id render-id}]))
+
+;; Outer alingmnent: display the shape in two layers. One
+;; without stroke (only fill), and another one only with stroke
+;; at double width (transparent fill) and passed through a mask
+;; that shows the whole shape, but hides the original shape
+;; without stroke
+(mf/defc outer-stroke
+  {::mf/wrap-props false}
+  [props]
+
+  (let [render-id    (mf/use-ctx muc/render-ctx)
+        child        (obj/get props "children")
+        base-props   (obj/get child "props")
+        elem-name    (obj/get child "type")
+        shape        (obj/get props "shape")
+        stroke-width (:stroke-width shape 0)
+        stroke-mask-id (str "outer-stroke-" render-id)
+        shape-id (str "stroke-shape-" render-id)
+
+        style-str (->> (obj/get base-props "style")
+                       (js->clj)
+                       (mapv (fn [[k v]]
+                               (-> (d/name k)
+                                   (str/kebab)
+                                   (str ":" v))))
+                       (str/join ";"))]
+
+    [:g.outer-stroke-shape
+     [:symbol
+      [:> elem-name (-> (obj/clone base-props)
+                        (obj/set! "id" shape-id)
+                        (obj/set! "data-style" style-str)
+                        (obj/without ["style"]))]]
+
+     [:use {:href (str "#" shape-id)
+            :mask (str "url(#" stroke-mask-id ")")
+            :style (-> (obj/get base-props "style")
+                       (obj/clone)
+                       (obj/update! "strokeWidth" * 2)
+                       (obj/without ["fill" "fillOpacity"])
+                       (obj/set! "fill" "none"))}]
+
+     [:use {:href (str "#" shape-id)
+            :style (-> (obj/get base-props "style")
+                       (obj/clone)
+                       (obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]]))
+
+
+;; Inner alignment: display the shape with double width stroke,
+;; and clip the result with the original shape without stroke.
+(mf/defc inner-stroke
+  {::mf/wrap-props false}
+  [props]
+  (let [render-id  (mf/use-ctx muc/render-ctx)
+        child      (obj/get props "children")
+        base-props (obj/get child "props")
+        elem-name  (obj/get child "type")
+        shape      (obj/get props "shape")
+        transform  (obj/get base-props "transform")
+
+        stroke-width (:stroke-width shape 0)
+
+        clip-id (str "inner-stroke-" render-id)
+        shape-id (str "stroke-shape-" render-id)
+
+        shape-props (-> base-props
+                        (add-props {:id shape-id
+                                    :transform nil
+                                    :clipPath (str "url('#" clip-id "')")})
+                        (add-style {:strokeWidth (* stroke-width 2)}))]
+
+    [:g.inner-stroke-shape {:transform transform}
+     [:> elem-name shape-props]]))
+
 
 ; The SVG standard does not implement yet the 'stroke-alignment'
 ; attribute, to define the position of the stroke relative to the
@@ -19,100 +139,25 @@
 (mf/defc shape-custom-stroke
   {::mf/wrap-props false}
   [props]
-  (let [shape (unchecked-get props "shape")
-        base-props (unchecked-get props "base-props")
-        elem-name (unchecked-get props "elem-name")
-        base-style (obj/get base-props "style")
-        {:keys [x y width height]} (:selrect shape)
-        stroke-id (mf/use-var (uuid/next))
+  (let [child (obj/get props "children")
+        shape (obj/get props "shape")
+        stroke-width (:stroke-width shape 0)
         stroke-style (:stroke-style shape :none)
-        stroke-position (:stroke-alignment shape :center)]
+        stroke-position (:stroke-alignment shape :center)
+        has-stroke? (and (and (> stroke-width 0)
+                              (not= stroke-style :none)))
+        inner? (= :inner stroke-position)
+        outer? (= :outer stroke-position)]
+
     (cond
-      ;; Center alignment (or no stroke): the default in SVG
-      (or (= stroke-style :none) (= stroke-position :center))
-      [:> elem-name (obj/merge! #js {} base-props)]
+      (and has-stroke? inner?)
+      [:& inner-stroke {:shape shape}
+       child]
 
-      ;; Inner alignment: display the shape with double width stroke,
-      ;; and clip the result with the original shape without stroke.
-      (= stroke-position :inner)
-      (let [clip-id (str "clip-" @stroke-id)
+      (and has-stroke? outer?)
+      [:& outer-stroke {:shape shape}
+       child]
 
-            clip-props (obj/merge
-                         base-props
-                         #js {:transform nil
-                              :style (obj/merge
-                                       base-style
-                                       #js {:stroke nil
-                                            :strokeWidth nil
-                                            :strokeOpacity nil
-                                            :strokeDasharray nil
-                                            :fill "white"
-                                            :fillOpacity 1})})
-
-            stroke-width (obj/get base-style "strokeWidth" 0)
-            shape-props (obj/merge
-                          base-props
-                          #js {:clipPath (str "url('#" clip-id "')")
-                               :style (obj/merge
-                                        base-style
-                                        #js {:strokeWidth (* stroke-width 2)})})]
-        [:*
-         [:> "clipPath" #js {:id clip-id}
-          [:> elem-name clip-props]]
-         [:> elem-name shape-props]])
-
-      ;; Outer alingmnent: display the shape in two layers. One
-      ;; without stroke (only fill), and another one only with stroke
-      ;; at double width (transparent fill) and passed through a mask
-      ;; that shows the whole shape, but hides the original shape
-      ;; without stroke
-
-      (= stroke-position :outer)
-      (let [stroke-mask-id (str "mask-" @stroke-id)
-            stroke-width   (obj/get base-style "strokeWidth" 0)
-            mask-props1 (obj/merge
-                          base-props
-                          #js {:transform nil
-                               :style (obj/merge
-                                        base-style
-                                        #js {:stroke "white"
-                                             :strokeWidth (* stroke-width 2)
-                                             :strokeOpacity 1
-                                             :strokeDasharray nil
-                                             :fill "white"
-                                             :fillOpacity 1})})
-            mask-props2 (obj/merge
-                          base-props
-                          #js {:transform nil
-                               :style (obj/merge
-                                        base-style
-                                        #js {:stroke nil
-                                             :strokeWidth nil
-                                             :strokeOpacity nil
-                                             :strokeDasharray nil
-                                             :fill "black"
-                                             :fillOpacity 1})})
-
-            shape-props1 (obj/merge
-                           base-props
-                           #js {:style (obj/merge
-                                         base-style
-                                         #js {:stroke nil
-                                              :strokeWidth nil
-                                              :strokeOpacity nil
-                                              :strokeDasharray nil})})
-            shape-props2 (obj/merge
-                           base-props
-                           #js {:mask (str "url('#" stroke-mask-id "')")
-                                :style (obj/merge
-                                         base-style
-                                         #js {:strokeWidth (* stroke-width 2)
-                                              :fill "none"
-                                              :fillOpacity 0})})]
-        [:*
-         [:mask {:id stroke-mask-id}
-          [:> elem-name mask-props1]
-          [:> elem-name mask-props2]]
-         [:> elem-name shape-props1]
-         [:> elem-name shape-props2]]))))
+      :else
+      child)))
 
diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs
index 256730eb2..972680305 100644
--- a/frontend/src/app/main/ui/shapes/path.cljs
+++ b/frontend/src/app/main/ui/shapes/path.cljs
@@ -26,16 +26,6 @@
         props (-> (attrs/extract-style-attrs shape)
                   (obj/merge!
                    #js {:d pdata}))]
-    (if background?
-      [:g
-       [:path {:stroke "none"
-               :fill "none"
-               :stroke-width "20px"
-               :d pdata}]
-       [:& shape-custom-stroke {:shape shape
-                                :base-props props
-                                :elem-name "path"}]]
-      [:& shape-custom-stroke {:shape shape
-                               :base-props props
-                               :elem-name "path"}])))
+    [:& shape-custom-stroke {:shape shape}
+     [:> :path props]]))
 
diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs
index aeb61b17a..bb0ccd60a 100644
--- a/frontend/src/app/main/ui/shapes/rect.cljs
+++ b/frontend/src/app/main/ui/shapes/rect.cljs
@@ -6,23 +6,19 @@
 
 (ns app.main.ui.shapes.rect
   (:require
-   [rumext.alpha :as mf]
+   [app.common.geom.shapes :as gsh]
    [app.main.ui.shapes.attrs :as attrs]
    [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
-   [app.common.geom.shapes :as geom]
-   [app.util.object :as obj]
    [app.main.ui.shapes.gradients :refer [gradient]]
-
-   [cuerdas.core :as str]
-   [app.common.uuid :as uuid]
-   [app.common.geom.point :as gpt]))
+   [app.util.object :as obj]
+   [rumext.alpha :as mf]))
 
 (mf/defc rect-shape
   {::mf/wrap-props false}
   [props]
   (let [shape (unchecked-get props "shape")
         {:keys [id x y width height]} shape
-        transform (geom/transform-matrix shape)
+        transform (gsh/transform-matrix shape)
 
         props (-> (attrs/extract-style-attrs shape)
                   (obj/merge!
@@ -30,11 +26,11 @@
                         :y y
                         :transform transform
                         :width width
-                        :height height}))]
+                        :height height}))
 
-    [:& shape-custom-stroke {:shape shape
-                             :base-props props
-                             :elem-name 
-                             (if (.-d props)
-                               "path"
-                               "rect")}]))
+        path? (some? (.-d props))]
+
+    [:& shape-custom-stroke {:shape shape}
+     (if path?
+       [:> :path props]
+       [:> :rect props])]))
diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs
index 82dddd238..5a8d4fecd 100644
--- a/frontend/src/app/main/ui/shapes/shape.cljs
+++ b/frontend/src/app/main/ui/shapes/shape.cljs
@@ -8,7 +8,9 @@
   (:require
    [app.common.data :as d]
    [app.common.uuid :as uuid]
+   [app.common.geom.matrix :as gmt]
    [app.main.ui.context :as muc]
+   [app.main.ui.shapes.custom-stroke :as cs]
    [app.main.ui.shapes.fill-image :as fim]
    [app.main.ui.shapes.filters :as filters]
    [app.main.ui.shapes.gradients :as grad]
@@ -16,6 +18,36 @@
    [app.util.object :as obj]
    [rumext.alpha :as mf]))
 
+(defn add-metadata
+  "Adds as metadata properties that we cannot deduce from the exported SVG"
+  [props shape]
+  (let [add!
+        (fn [props attr val]
+          (let [ns-attr (str "penpot:" (-> attr d/name))]
+            (-> props
+                (obj/set! ns-attr val))))
+        frame? (= :frame type)]
+    (-> props
+        (add! :name              (-> shape :name))
+        (add! :blocked           (-> shape (:blocked false) str))
+        (add! :hidden            (-> shape (:hidden false) str))
+        (add! :type              (-> shape :type d/name))
+
+        (add! :stroke-style      (-> shape (:stroke-style :none) d/name))
+        (add! :stroke-alignment  (-> shape (:stroke-alignment :center) d/name))
+
+        (add! :transform         (-> shape (:transform (gmt/matrix)) str))
+        (add! :transform-inverse (-> shape (:transform-inverse (gmt/matrix)) str))
+
+        (cond-> (some? (:r1 shape))
+          (-> (add! :r1 (-> shape (:r1 0) str))
+              (add! :r2 (-> shape (:r2 0) str))
+              (add! :r3 (-> shape (:r3 0) str))
+              (add! :r4 (-> shape (:r4 0) str))))
+
+        (cond-> frame?
+          (obj/set! "xmlns:penpot" "https://penpot.app/xmlns")))))
+
 (mf/defc shape-container
   {::mf/forward-ref true
    ::mf/wrap-props false}
@@ -33,28 +65,34 @@
 
         {:keys [x y width height type]} shape
         frame? (= :frame type)
-        group-props (-> (obj/clone props)
-                        (obj/without ["shape" "children"])
-                        (obj/set! "ref" ref)
-                        (obj/set! "id" (str "shape-" (:id shape)))
-                        (obj/set! "filter" (filters/filter-str filter-id shape))
-                        (obj/set! "style" styles)
 
-                        (cond-> frame?
-                          (-> (obj/set! "x" x)
-                              (obj/set! "y" y)
-                              (obj/set! "width" width)
-                              (obj/set! "height" height)
-                              (obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink")
-                              (obj/set! "xmlns" "http://www.w3.org/2000/svg"))))
+        wrapper-props
+        (-> (obj/clone props)
+            (obj/without ["shape" "children"])
+            (obj/set! "ref" ref)
+            (obj/set! "id" (str "shape-" (:id shape)))
+            (obj/set! "filter" (filters/filter-str filter-id shape))
+            (obj/set! "style" styles)
+
+            (cond-> frame?
+              (-> (obj/set! "x" x)
+                  (obj/set! "y" y)
+                  (obj/set! "width" width)
+                  (obj/set! "height" height)
+                  (obj/set! "xmlnsXlink" "http://www.w3.org/1999/xlink")
+                  (obj/set! "xmlns" "http://www.w3.org/2000/svg")))
+
+            (add-metadata shape))
 
         wrapper-tag (if frame? "svg" "g")]
+
     [:& (mf/provider muc/render-ctx) {:value render-id}
-     [:> wrapper-tag group-props
+     [:> wrapper-tag wrapper-props
       [:defs
        [:& defs/svg-defs          {:shape shape :render-id render-id}]
        [:& filters/filters        {:shape shape :filter-id filter-id}]
        [:& grad/gradient          {:shape shape :attr :fill-color-gradient}]
        [:& grad/gradient          {:shape shape :attr :stroke-color-gradient}]
-       [:& fim/fill-image-pattern {:shape shape :render-id render-id}]]
+       [:& fim/fill-image-pattern {:shape shape :render-id render-id}]
+       [:& cs/stroke-defs         {:shape shape :render-id render-id}]]
       children]]))
diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs
index 22232ca01..c9183c3fe 100644
--- a/frontend/src/app/main/ui/workspace/shapes/path.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs
@@ -28,5 +28,4 @@
 
     [:> shape-container {:shape shape
                          :pointer-events (when editing? "none")}
-     [:& path/path-shape {:shape shape
-                          :background? true}]]))
+     [:& path/path-shape {:shape shape}]]))
diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
index 380c96f77..e65bc12d0 100644
--- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
+++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs
@@ -278,7 +278,6 @@
                            (let [point (gpt/transform point transform-inverse)
                                  end-x (/ (- (:x point) x) width)
                                  end-y (/ (- (:y point) y) height)
-
                                  end-x (mth/precision end-x 2)
                                  end-y (mth/precision end-y 2)]
                              (change! {:end-x end-x :end-y end-y})))
@@ -287,8 +286,8 @@
                           (let [scale-factor-y (/ gradient-length (/ height 2))
                                 norm-dist (/ (gpt/distance point from-p)
                                              (* (/ width 2) scale-factor-y))]
-
-                            (change! {:width norm-dist})))]
+                            (when (and norm-dist (mth/finite? norm-dist))
+                              (change! {:width norm-dist}))))]
 
     (when (and gradient
                (= id (:shape-id gradient))
diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs
index 5864e285c..b5acc982a 100644
--- a/frontend/src/app/util/dom.cljs
+++ b/frontend/src/app/util/dom.cljs
@@ -8,10 +8,11 @@
   (:require
    [app.common.exceptions :as ex]
    [app.common.geom.point :as gpt]
-   [app.util.object :as obj]
    [app.util.globals :as globals]
+   [app.util.object :as obj]
    [cuerdas.core :as str]
-   [goog.dom :as dom]))
+   [goog.dom :as dom]
+   [promesa.core :as p]))
 
 ;; --- Deprecated methods
 
@@ -333,4 +334,23 @@
 
 (defn trigger-download
   [filename blob]
-  (trigger-download-uri filename (.-type ^js blob) (dom/create-uri blob)))
+  (trigger-download-uri filename (.-type ^js blob) (create-uri blob)))
+
+(defn save-as
+  [uri filename mtype description]
+
+  ;; Only chrome supports the save dialog
+  (if (obj/contains? globals/window "showSaveFilePicker")
+    (let [extension (mtype->extension mtype)
+          opts {:suggestedName (str filename "." extension)
+                :types [{:description description
+                         :accept { mtype [(str "." extension)]}}]}]
+
+      (p/let [file-system (.showSaveFilePicker globals/window (clj->js opts))
+              writable    (.createWritable file-system)
+              response    (js/fetch uri)
+              blob        (.blob response)
+              _           (.write writable blob)]
+        (.close writable)))
+
+    (trigger-download-uri filename mtype uri)))
diff --git a/frontend/src/app/util/import/parser.cljc b/frontend/src/app/util/import/parser.cljc
new file mode 100644
index 000000000..6847b194f
--- /dev/null
+++ b/frontend/src/app/util/import/parser.cljc
@@ -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) UXBOX Labs SL
+
+(ns app.util.import.parser
+  (:require
+   [app.common.data :as d]
+   [app.common.geom.matrix :as gmt]
+   [app.common.geom.shapes :as gsh]
+   [cuerdas.core :as str]
+   [app.util.path.parser :as upp]))
+
+(defn valid?
+  [root]
+  (contains? (:attrs root) :xmlns:penpot))
+
+(defn branch?
+  [node]
+  (and (contains? node :content)
+       (some? (:content node))))
+
+(defn close?
+  [node]
+  (and (vector? node)
+       (= ::close (first node))))
+
+(defn get-type
+  [node]
+  (if (close? node)
+    (second node)
+    (-> (get-in node [:attrs :penpot:type])
+        (keyword))))
+
+(defn shape?
+  [node]
+  (or (close? node)
+      (contains? (:attrs node) :penpot:type)))
+
+(defn get-attr
+  ([m att]
+   (get-attr m att identity))
+  ([m att val-fn]
+   (let [ns-att (->> att d/name (str "penpot:") keyword)
+         val (get-in m [:attrs ns-att])]
+     (when val (val-fn val)))))
+
+(defn get-children
+  [node]
+  (cond-> (:content node)
+    ;; We add a "fake" node to know when we are leaving the shape children
+    (shape? node)
+    (conj [::close (get-type node)])))
+
+(defn node-seq
+  [content]
+  (->> content (tree-seq branch? get-children)))
+
+(defn get-transform
+  [type node])
+
+(defn parse-style
+  "Transform style list into a map"
+  [style-str]
+  (if (string? style-str)
+    (->> (str/split style-str ";")
+         (map str/trim)
+         (map #(str/split % ":"))
+         (group-by first)
+         (map (fn [[key val]]
+                (vector (keyword key) (second (first val)))))
+         (into {}))
+    style-str))
+
+(defn add-attrs
+  [m attrs]
+  (reduce-kv
+   (fn [m k v]
+     (if (#{:style :data-style} k)
+       (assoc m :style (parse-style v))
+       (assoc m k v)))
+   m
+   attrs))
+
+(defn get-data-node
+  [node]
+
+  (let [data-tags #{:ellipse :rect :path}]
+    (->> node
+         (node-seq)
+         (filter #(contains? data-tags (:tag %)))
+         (map #(:attrs %))
+         (reduce add-attrs {}))))
+
+(def search-data-node? #{:rect :image :path :text :circle})
+(def has-position? #{:frame :rect :image :text})
+
+(defn parse-position
+  [props data]
+  (let [values (->> (select-keys data [:x :y :width :height])
+                    (d/mapm (fn [_ val] (d/parse-double val))))]
+    (d/merge props values)))
+
+(defn parse-circle
+  [props data]
+  (let [values (->> (select-keys data [:cx :cy :rx :ry])
+                    (d/mapm (fn [_ val] (d/parse-double val))))]
+
+    {:x (- (:cx values) (:rx values))
+     :y (- (:cy values) (:ry values))
+     :width (* (:rx values) 2)
+     :height (* (:ry values) 2)}))
+
+(defn parse-path
+  [props data]
+  (let [content (upp/parse-path (:d data))
+        selrect (gsh/content->selrect content)
+        points (gsh/rect->points selrect)]
+
+    (-> props
+        (assoc :content content)
+        (assoc :selrect selrect)
+        (assoc :points points))))
+
+(defn extract-data
+  [type node]
+  (let [data (if (search-data-node? type)
+               (get-data-node node)
+               (:attrs node))]
+    (cond-> {}
+      (has-position? type)
+      (-> (parse-position data)
+          (gsh/setup-selrect))
+
+      (= type :circle)
+      (-> (parse-circle data)
+          (gsh/setup-selrect))
+
+      (= type :path)
+      (parse-path data))))
+
+(defn str->bool
+  [val]
+  (= val "true"))
+
+(defn parse-data
+  [type node]
+
+  (when-not (close? node)
+    (let [name              (get-attr node :name)
+          blocked           (get-attr node :blocked str->bool)
+          hidden            (get-attr node :hidden str->bool)
+          transform         (get-attr node :transform gmt/str->matrix)
+          transform-inverse (get-attr node :transform-inverse gmt/str->matrix)]
+
+      (-> (extract-data type node)
+          (assoc :name name)
+          (assoc :blocked blocked)
+          (assoc :hidden hidden)
+          (cond-> (some? transform)
+            (assoc :transform transform))
+          (cond-> (some? transform-inverse)
+            (assoc :transform-inverse transform-inverse))))))
diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs
index ad6697a42..12aabcb60 100644
--- a/frontend/src/app/util/object.cljs
+++ b/frontend/src/app/util/object.cljs
@@ -6,7 +6,7 @@
 
 (ns app.util.object
   "A collection of helpers for work with javascript objects."
-  (:refer-clojure :exclude [set! get get-in merge clone])
+  (:refer-clojure :exclude [set! get get-in merge clone contains?])
   (:require
    [cuerdas.core :as str]
    [goog.object :as gobj]
@@ -22,22 +22,27 @@
    (let [result (get obj k)]
      (if (undefined? result) default result))))
 
+(defn contains?
+  [obj k]
+  (some? (unchecked-get obj k)))
+
 (defn get-keys
   [obj]
   (js/Object.keys ^js obj))
 
 (defn get-in
-  [obj keys]
-  (loop [key (first keys)
-         keys (rest keys)
-         res obj]
-    (if (nil? key)
-      res
-      (if (nil? res)
-        res
-        (recur (first keys)
-               (rest keys)
-               (unchecked-get res key))))))
+  ([obj keys]
+   (get-in obj keys nil))
+
+  ([obj keys default]
+   (loop [key (first keys)
+          keys (rest keys)
+          res obj]
+     (if (or (nil? key) (nil? res))
+       (or res default)
+       (recur (first keys)
+              (rest keys)
+              (unchecked-get res key))))))
 
 (defn without
   [obj keys]
@@ -68,6 +73,14 @@
   (unchecked-set obj key value)
   obj)
 
+(defn update!
+  [obj key f & args]
+  (let [found (get obj key ::not-found)]
+    (if-not (identical? ::not-found found)
+      (do (unchecked-set obj key (apply f found args))
+          obj)
+      obj)))
+
 (defn- props-key-fn
   [key]
   (if (or (= key :class) (= key :class-name))
diff --git a/frontend/src/app/util/path/parser.cljs b/frontend/src/app/util/path/parser.cljs
index 09f491555..fc23adc61 100644
--- a/frontend/src/app/util/path/parser.cljs
+++ b/frontend/src/app/util/path/parser.cljs
@@ -9,14 +9,13 @@
    [app.common.data :as d]
    [app.common.geom.point :as gpt]
    [app.common.geom.shapes.path :as gshp]
+   [app.common.math :as mth]
    [app.util.path.arc-to-curve :refer [a2c]]
    [app.util.path.commands :as upc]
-   [app.util.svg :as usvg]
-   [cuerdas.core :as str]
-   [clojure.set :as set]
-   [app.common.math :as mth]
    [app.util.path.geom :as upg]
-   ))
+   [app.util.svg :as usvg]
+   [clojure.set :as set]
+   [cuerdas.core :as str]))
 
 ;;
 (def commands-regex #"(?i)[mzlhvcsqta][^mzlhvcsqta]*")
diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs
index 48e64746e..12c9e0d97 100644
--- a/frontend/src/app/util/svg.cljs
+++ b/frontend/src/app/util/svg.cljs
@@ -745,8 +745,6 @@
       (reduce gmt/multiply (gmt/matrix) matrices))
     (gmt/matrix)))
 
-
-
 (defn format-move [[x y]] (str "M" x " " y))
 (defn format-line [[x y]] (str "L" x " " y))
 
diff --git a/frontend/src/app/worker.cljs b/frontend/src/app/worker.cljs
index 05d9ffc51..44cd628f6 100644
--- a/frontend/src/app/worker.cljs
+++ b/frontend/src/app/worker.cljs
@@ -6,6 +6,7 @@
 
 (ns app.worker
   (:require
+
    [app.common.exceptions :as ex]
    [app.common.spec :as us]
    [app.common.transit :as t]
@@ -14,6 +15,9 @@
    [app.util.worker :as w]
    [app.worker.impl :as impl]
    [app.worker.selection]
+
+   [app.worker.import]
+   [app.worker.export]
    [app.worker.snaps]
    [app.worker.thumbnails]
    [beicon.core :as rx]
@@ -159,4 +163,3 @@
   (set! process-message-sub (subscribe-buffer-messages))
   (.addEventListener js/self "message" on-message))
 
-
diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs
new file mode 100644
index 000000000..1d1dcb108
--- /dev/null
+++ b/frontend/src/app/worker/export.cljs
@@ -0,0 +1,56 @@
+;; 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.worker.export
+  (:require
+   [app.main.render :as r]
+   [app.main.repo :as rp]
+   [app.util.dom :as dom]
+   [app.util.zip :as uz]
+   [app.worker.impl :as impl]
+   [beicon.core :as rx]))
+
+(defn get-page-data
+  [{file-name :file-name {:keys [id name] :as data} :data}]
+  (->> (r/render-page data)
+       (rx/map (fn [markup]
+                 {:id id
+                  :name name
+                  :file-name file-name
+                  :markup markup}))))
+
+(defn process-pages [file]
+  (let [pages (get-in file [:data :pages])
+        pages-index (get-in file [:data :pages-index])]
+    (->> pages
+         (map #(hash-map
+                :file-name (:name file)
+                :data (get pages-index %))))))
+
+(defn collect-page
+  [coll {:keys [id file-name name markup] :as page}]
+  (conj coll [(str file-name "/" name ".svg") markup]))
+
+(defmethod impl/handler :export-file
+  [{:keys [team-id files] :as message}]
+
+  (let [render-stream
+        (->> (rx/from (->> files (mapv :id)))
+             (rx/merge-map #(rp/query :file {:id %}))
+             (rx/flat-map process-pages)
+             (rx/observe-on :async)
+             (rx/flat-map get-page-data)
+             (rx/share))]
+
+    (rx/merge
+     (->> render-stream
+          (rx/map #(hash-map :type :progress
+                             :data (str "Render " (:file-name %) " - " (:name %)))))
+     (->> render-stream
+          (rx/reduce collect-page [])
+          (rx/flat-map uz/compress-files)
+          (rx/map #(hash-map :type :finish
+                             :data (dom/create-uri %)))))))
diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs
new file mode 100644
index 000000000..350035ed9
--- /dev/null
+++ b/frontend/src/app/worker/import.cljs
@@ -0,0 +1,143 @@
+;; 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.worker.import
+  (:require
+   [app.common.data :as d]
+   [app.common.file-builder :as fb]
+   [app.common.pages :as cp]
+   [app.common.uuid :as uuid]
+   [app.main.repo :as rp]
+   [app.util.import.parser :as cip]
+   [app.util.zip :as uz]
+   [app.worker.impl :as impl]
+   [beicon.core :as rx]
+   [cuerdas.core :as str]
+   [tubax.core :as tubax]))
+
+;; Upload changes batches size
+(def change-batch-size 100)
+
+(defn create-empty-file
+  "Create a new file on the back-end"
+  [project-id file]
+  (rp/mutation
+   :create-file
+   {:id (:id file)
+    :name (:name file)
+    :project-id project-id
+    :data (-> cp/empty-file-data
+              (assoc :id (:id file)))}))
+
+(defn send-changes
+  "Creates batches of changes to be sent to the backend"
+  [file init-revn]
+  (let [revn (atom init-revn)
+        file-id (:id file)
+        session-id (uuid/next)
+        changes-batches
+        (->> (fb/generate-changes file)
+             (partition change-batch-size change-batch-size nil)
+             (mapv vec))]
+
+    (->> (rx/from changes-batches)
+         (rx/merge-map
+          (fn [cur-changes-batch]
+            (rp/mutation
+             :update-file
+             {:id file-id
+              :session-id session-id
+              :revn @revn
+              :changes cur-changes-batch})))
+
+         (rx/tap #(reset! revn (:revn %))))))
+
+(defn persist-file
+  "Sends to the back-end the imported data"
+  [project-id file]
+  (->> (create-empty-file project-id file)
+       (rx/flat-map #(send-changes file (:revn %)))))
+
+(defn parse-file-name
+  [dir]
+  (if (str/ends-with? dir "/")
+    (subs dir 0 (dec (count dir)))
+    dir))
+
+(defn parse-page-name
+  [path]
+  (let [[file page] (str/split path "/")]
+    (str/replace page ".svg" "")))
+
+(defn add-shape-file
+  [file node]
+
+  (let [type   (cip/get-type node)
+        close? (cip/close? node)
+        data   (cip/parse-data type node)]
+
+    (if close?
+      (case type
+        :frame
+        (fb/close-artboard file)
+
+        :group
+        (fb/close-group file)
+
+        ;; default
+        file)
+
+      (case type
+        :frame    (fb/add-artboard file data)
+        :group    (fb/add-group file data)
+        :rect     (fb/create-rect file data)
+        :circle   (fb/create-circle file data)
+        :path     (fb/create-path file data)
+        :text     (fb/create-text file data)
+        :image    (fb/create-image file data)
+
+        ;; default
+        file))))
+
+(defn import-page
+  [file {:keys [path content]}]
+  (let [page-name (parse-page-name path)]
+    (when (cip/valid? content)
+      (let [nodes (->> content cip/node-seq)]
+        (->> nodes
+             (filter cip/shape?)
+             (reduce add-shape-file (fb/add-page file page-name))
+             (fb/close-page))))))
+
+(defmethod impl/handler :import-file
+  [{:keys [project-id files]}]
+
+  (let [extract-stream
+        (->> (rx/from files)
+             (rx/merge-map uz/extract-files))
+
+        dir-str
+        (->> extract-stream
+             (rx/filter #(contains? % :dir))
+             (rx/map :dir))
+
+        file-str
+        (->> extract-stream
+             (rx/filter #(not (contains? % :dir)))
+             (rx/map #(d/update-when % :content tubax/xml->clj)))]
+
+    (->> dir-str
+         (rx/merge-map
+          (fn [dir]
+            (let [file (fb/create-file (parse-file-name dir))]
+              (rx/concat
+               (->> file-str
+                    (rx/filter #(str/starts-with? (:path %) dir))
+                    (rx/reduce import-page file)
+                    (rx/flat-map #(persist-file project-id %))
+                    (rx/ignore))
+
+               (rx/of (select-keys file [:id :name])))))))))
diff --git a/frontend/translations/en.po b/frontend/translations/en.po
index 1721be209..31d41d091 100644
--- a/frontend/translations/en.po
+++ b/frontend/translations/en.po
@@ -2633,4 +2633,10 @@ msgid "workspace.updates.update"
 msgstr "Update"
 
 msgid "workspace.viewport.click-to-close-path"
-msgstr "Click to close the path"
\ No newline at end of file
+msgstr "Click to close the path"
+
+msgid "dashboard.export-single"
+msgstr "Export file"
+
+msgid "dashboard.export-multi"
+msgstr "Export %s files"