diff --git a/bin.mjs b/bin.mjs
index fab6e91..5cfad11 100644
--- a/bin.mjs
+++ b/bin.mjs
@@ -104,6 +104,7 @@ try {
       const res = await apply(notionPath, {
         overwritePrevious: promptRes,
         takeBackup: opts.get('no-backup') ? false : true,
+        applyDevPatch: opts.get('dev-patch') ? true : false,
       });
       if (res) {
         log`{bold.rgb(245,245,245) SUCCESS} {green ✔}`;
diff --git a/insert/api b/insert/api
index aff6f2d..465e5a1 160000
--- a/insert/api
+++ b/insert/api
@@ -1 +1 @@
-Subproject commit aff6f2dafa9d2666306f4e088da86528aeee0cd8
+Subproject commit 465e5a10ccf526ea0f6567650c0603d44ecd631c
diff --git a/insert/client.mjs b/insert/client.mjs
new file mode 100644
index 0000000..a0c6d71
--- /dev/null
+++ b/insert/client.mjs
@@ -0,0 +1,34 @@
+/*
+ * notion-enhancer core: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+(async () => {
+  const page = location.pathname.split(/[/-]/g).reverse()[0].length === 32,
+    whitelisted = ['/', '/onboarding'].includes(location.pathname),
+    signedIn = localStorage['LRU:KeyValueStore2:current-user-id'];
+
+  if (page || (whitelisted && signedIn)) {
+    const api = await import('./api/_.mjs'),
+      { fs, registry, web } = api;
+
+    for (const mod of await registry.list((mod) => registry.enabled(mod.id))) {
+      for (const sheet of mod.css?.client || []) {
+        web.loadStylesheet(`repo/${mod._dir}/${sheet}`);
+      }
+      for (let script of mod.js?.client || []) {
+        script = await import(fs.localPath(`repo/${mod._dir}/${script}`));
+        script.default(api, await registry.db(mod.id));
+      }
+    }
+
+    const errors = await registry.errors();
+    if (errors.length) {
+      console.log('[notion-enhancer] registry errors:');
+      console.table(errors);
+    }
+  }
+})();
diff --git a/insert/dep b/insert/dep
index 9a3893f..65ae194 160000
--- a/insert/dep
+++ b/insert/dep
@@ -1 +1 @@
-Subproject commit 9a3893fbd5af4d02b89ea4c6f2b35971a3a91408
+Subproject commit 65ae1944a75b9525ee79610586438facf14d8531
diff --git a/insert/env/env.cjs b/insert/env/env.cjs
new file mode 100644
index 0000000..e62fc1c
--- /dev/null
+++ b/insert/env/env.cjs
@@ -0,0 +1,48 @@
+/*
+ * notion-enhancer core: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+/**
+ * environment-specific methods and constants
+ * @module notion-enhancer/api/env
+ */
+
+module.exports = {};
+
+/**
+ * the environment/platform name code is currently being executed in
+ * @constant
+ * @type {string}
+ */
+module.exports.name = process.platform;
+
+/**
+ * the current version of the enhancer
+ * @constant
+ * @type {string}
+ */
+module.exports.version = require('notion-enhancer/package.json').version;
+
+/**
+ * open the enhancer's menu
+ * @type {function}
+ */
+module.exports.focusMenu = () => console.log(1);
+//  window.__enhancerElectronApi.sendMessage({ action: 'focusMenu' });
+
+/**
+ * focus an active notion tab
+ * @type {function}
+ */
+module.exports.focusNotion = () => console.log(1);
+//  window.__enhancerElectronApi.sendMessage({ action: 'focusNotion' });
+
+/**
+ * reload all notion and enhancer menu tabs to apply changes
+ * @type {function}
+ */
+module.exports.reload = () => console.log(1); // window.__enhancerElectronApi.sendMessage({ action: 'reload' });
diff --git a/insert/env/env.mjs b/insert/env/env.mjs
index 705c578..5d41e54 100644
--- a/insert/env/env.mjs
+++ b/insert/env/env.mjs
@@ -16,29 +16,31 @@
  * @constant
  * @type {string}
  */
-export const name = process.platform;
+export const name = window.__enhancerElectronApi.platform;
 
 /**
  * the current version of the enhancer
  * @constant
  * @type {string}
  */
-export const version = chrome.runtime.getManifest().version;
+export const version = window.__enhancerElectronApi.version;
 
 /**
  * open the enhancer's menu
  * @type {function}
  */
-export const focusMenu = () => chrome.runtime.sendMessage({ action: 'focusMenu' });
+export const focusMenu = () =>
+  window.__enhancerElectronApi.sendMessage({ action: 'focusMenu' });
 
 /**
  * focus an active notion tab
  * @type {function}
  */
-export const focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' });
+export const focusNotion = () =>
+  window.__enhancerElectronApi.sendMessage({ action: 'focusNotion' });
 
 /**
  * reload all notion and enhancer menu tabs to apply changes
  * @type {function}
  */
-export const reload = () => chrome.runtime.sendMessage({ action: 'reload' });
+export const reload = () => window.__enhancerElectronApi.sendMessage({ action: 'reload' });
diff --git a/insert/env/fs.cjs b/insert/env/fs.cjs
new file mode 100644
index 0000000..0bc3071
--- /dev/null
+++ b/insert/env/fs.cjs
@@ -0,0 +1,53 @@
+/*
+ * notion-enhancer core: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+/**
+ * environment-specific file reading
+ * @module notion-enhancer/api/fs
+ */
+
+module.exports = {};
+
+/**
+ * transform a path relative to the enhancer root directory into an absolute path
+ * @param {string} path - a url or within-the-enhancer filepath
+ * @returns {string} an absolute filepath
+ */
+module.exports.localPath = (path) => `notion://www.notion.so/__notion-enhancer/${path}`;
+
+/**
+ * fetch and parse a json file's contents
+ * @param {string} path - a url or within-the-enhancer filepath
+ * @param {object} [opts] - the second argument of a fetch() request
+ * @returns {object} the json value of the requested file as a js object
+ */
+module.exports.getJSON = (path, opts = {}) =>
+  fetch(path.startsWith('http') ? path : localPath(path), opts).then((res) => res.json());
+
+/**
+ * fetch a text file's contents
+ * @param {string} path - a url or within-the-enhancer filepath
+ * @param {object} [opts] - the second argument of a fetch() request
+ * @returns {string} the text content of the requested file
+ */
+module.exports.getText = (path, opts = {}) =>
+  fetch(path.startsWith('http') ? path : localPath(path), opts).then((res) => res.text());
+
+/**
+ * check if a file exists
+ * @param {string} path - a url or within-the-enhancer filepath
+ * @returns {boolean} whether or not the file exists
+ */
+module.exports.isFile = async (path) => {
+  try {
+    await fetch(path.startsWith('http') ? path : localPath(path));
+    return true;
+  } catch {
+    return false;
+  }
+};
diff --git a/insert/env/fs.mjs b/insert/env/fs.mjs
index 376440f..5b6cdcb 100644
--- a/insert/env/fs.mjs
+++ b/insert/env/fs.mjs
@@ -7,7 +7,7 @@
 'use strict';
 
 /**
- * environment-specific filesystem reading
+ * environment-specific file reading
  * @module notion-enhancer/api/fs
  */
 
@@ -16,7 +16,7 @@
  * @param {string} path - a url or within-the-enhancer filepath
  * @returns {string} an absolute filepath
  */
-export const localPath = chrome.runtime.getURL;
+export const localPath = (path) => `notion://www.notion.so/__notion-enhancer/${path}`;
 
 /**
  * fetch and parse a json file's contents
diff --git a/insert/env/storage.cjs b/insert/env/storage.cjs
new file mode 100644
index 0000000..307c763
--- /dev/null
+++ b/insert/env/storage.cjs
@@ -0,0 +1,137 @@
+/*
+ * notion-enhancer core: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+/**
+ * environment-specific data persistence
+ * @module notion-enhancer/api/storage
+ */
+
+module.exports = {};
+
+const _queue = [],
+  _onChangeListeners = [];
+
+const os = require('os'),
+  path = require('path'),
+  fs = require('fs'),
+  _cacheFile = path.resolve(`${os.homedir()}/.notion-enhancer`);
+
+// handle leftover cache from prev versions
+if (fs.existsSync(_cacheFile) && fs.lstatSync(_cacheFile).isDirectory()) {
+  fs.rmdirSync(_cacheFile);
+}
+if (!fs.existsSync(_cacheFile)) fs.writeFileSync(_cacheFile, '{}', 'utf8');
+
+const getData = () => {
+    try {
+      return JSON.parse(fs.readFileSync(_cacheFile));
+    } catch (err) {
+      return {};
+    }
+  },
+  saveData = (data) => fs.writeFileSync(_cacheFile, JSON.stringify(data));
+
+/**
+ * get persisted data
+ * @param {array<string>} path - the path of keys to the value being fetched
+ * @param {*} [fallback] - a default value if the path is not matched
+ * @returns {Promise} value ?? fallback
+ */
+module.exports.get = (path, fallback = undefined) => {
+  if (!path.length) return fallback;
+  const values = getData();
+  let value = values;
+  while (path.length) {
+    if (value === undefined) {
+      value = fallback;
+      break;
+    }
+    value = value[path.shift()];
+  }
+  return Promise.resolve(value ?? fallback);
+};
+
+/**
+ * persist data
+ * @param {array<string>} path - the path of keys to the value being set
+ * @param {*} value - the data to save
+ * @returns {Promise} resolves when data has been saved
+ */
+module.exports.set = (path, value) => {
+  if (!path.length) return undefined;
+  const precursor = _queue[_queue.length - 1] || undefined,
+    interaction = new Promise(async (res, rej) => {
+      if (precursor !== undefined) {
+        await precursor;
+        _queue.shift();
+      }
+      const pathClone = [...path],
+        values = getData();
+      let pointer = values,
+        old;
+      while (path.length) {
+        const key = path.shift();
+        if (!path.length) {
+          old = pointer[key];
+          pointer[key] = value;
+          break;
+        }
+        pointer[key] = pointer[key] ?? {};
+        pointer = pointer[key];
+      }
+      saveData(values);
+      _onChangeListeners.forEach((listener) =>
+        listener({ type: 'set', path: pathClone, new: value, old })
+      );
+      res(value);
+    });
+  _queue.push(interaction);
+  return interaction;
+};
+
+/**
+ * create a wrapper for accessing a partition of the storage
+ * @param {array<string>} namespace - the path of keys to prefix all storage requests with
+ * @param {function} [get] - the storage get function to be wrapped
+ * @param {function} [set] - the storage set function to be wrapped
+ * @returns {object} an object with the wrapped get/set functions
+ */
+module.exports.db = (namespace, getFunc = get, setFunc = set) => {
+  if (typeof namespace === 'string') namespace = [namespace];
+  return {
+    get: (path = [], fallback = undefined) => getFunc([...namespace, ...path], fallback),
+    set: (path, value) => setFunc([...namespace, ...path], value),
+  };
+};
+
+/**
+ * add an event listener for changes in storage
+ * @param {onStorageChangeCallback} callback - called whenever a change in
+ * storage is initiated from the current process
+ */
+module.exports.addChangeListener = (callback) => {
+  _onChangeListeners.push(callback);
+};
+
+/**
+ * remove a listener added with storage.addChangeListener
+ * @param {onStorageChangeCallback} callback
+ */
+module.exports.removeChangeListener = (callback) => {
+  _onChangeListeners = _onChangeListeners.filter((listener) => listener !== callback);
+};
+
+/**
+ * @callback onStorageChangeCallback
+ * @param {object} event
+ * @param {string} event.type - 'set' or 'reset'
+ * @param {string} event.namespace- the name of the store, e.g. a mod id
+ * @param {string} [event.key] - the key associated with the changed value
+ * @param {string} [event.new] - the new value being persisted to the store
+ * @param {string} [event.old] - the previous value associated with the key
+ */
diff --git a/insert/env/storage.mjs b/insert/env/storage.mjs
index 3003231..a4cc72f 100644
--- a/insert/env/storage.mjs
+++ b/insert/env/storage.mjs
@@ -11,9 +11,6 @@
  * @module notion-enhancer/api/storage
  */
 
-const _queue = [],
-  _onChangeListeners = [];
-
 /**
  * get persisted data
  * @param {array<string>} path - the path of keys to the value being fetched
@@ -21,20 +18,7 @@ const _queue = [],
  * @returns {Promise} value ?? fallback
  */
 export const get = (path, fallback = undefined) => {
-  if (!path.length) return fallback;
-  return new Promise((res, rej) =>
-    chrome.storage.local.get(async (values) => {
-      let value = values;
-      while (path.length) {
-        if (value === undefined) {
-          value = fallback;
-          break;
-        }
-        value = value[path.shift()];
-      }
-      res(value ?? fallback);
-    })
-  );
+  return window.__enhancerElectronApi.db.get(path, fallback);
 };
 
 /**
@@ -44,38 +28,7 @@ export const get = (path, fallback = undefined) => {
  * @returns {Promise} resolves when data has been saved
  */
 export const set = (path, value) => {
-  if (!path.length) return undefined;
-  const precursor = _queue[_queue.length - 1] || undefined,
-    interaction = new Promise(async (res, rej) => {
-      if (precursor !== undefined) {
-        await precursor;
-        _queue.shift();
-      }
-      const pathClone = [...path],
-        namespace = path[0];
-      chrome.storage.local.get(async (values) => {
-        let pointer = values,
-          old;
-        while (path.length) {
-          const key = path.shift();
-          if (!path.length) {
-            old = pointer[key];
-            pointer[key] = value;
-            break;
-          }
-          pointer[key] = pointer[key] ?? {};
-          pointer = pointer[key];
-        }
-        chrome.storage.local.set({ [namespace]: values[namespace] }, () => {
-          _onChangeListeners.forEach((listener) =>
-            listener({ type: 'set', path: pathClone, new: value, old })
-          );
-          res(value);
-        });
-      });
-    });
-  _queue.push(interaction);
-  return interaction;
+  return window.__enhancerElectronApi.db.set(path, value);
 };
 
 /**
@@ -99,7 +52,7 @@ export const db = (namespace, getFunc = get, setFunc = set) => {
  * storage is initiated from the current process
  */
 export const addChangeListener = (callback) => {
-  _onChangeListeners.push(callback);
+  return window.__enhancerElectronApi.db.addChangeListener(callback);
 };
 
 /**
@@ -107,7 +60,7 @@ export const addChangeListener = (callback) => {
  * @param {onStorageChangeCallback} callback
  */
 export const removeChangeListener = (callback) => {
-  _onChangeListeners = _onChangeListeners.filter((listener) => listener !== callback);
+  return window.__enhancerElectronApi.db.removeChangeListener(callback);
 };
 
 /**
diff --git a/insert/init.cjs b/insert/init.cjs
new file mode 100644
index 0000000..46680ec
--- /dev/null
+++ b/insert/init.cjs
@@ -0,0 +1,33 @@
+/*
+ * notion-enhancer core: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+const api = require('notion-enhancer/api/_.cjs');
+
+module.exports = async function (target, __exports) {
+  if (target === 'renderer/preload') {
+    window.__enhancerElectronApi = {
+      platform: process.platform,
+      version: require('notion-enhancer/package.json').version,
+      db: {
+        get: api.storage.get,
+        set: api.storage.set,
+        addChangeListener: api.storage.addChangeListener,
+        removeChangeListener: api.storage.removeChangeListener,
+      },
+      sendMessage: (message) => {},
+    };
+
+    document.addEventListener('readystatechange', (event) => {
+      if (document.readyState !== 'complete') return false;
+      const script = document.createElement('script');
+      script.type = 'module';
+      script.src = 'notion://www.notion.so/__notion-enhancer/client.mjs';
+      document.head.appendChild(script);
+    });
+  }
+};
diff --git a/insert/init.js b/insert/init.js
deleted file mode 100644
index 1cd0040..0000000
--- a/insert/init.js
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * notion-enhancer
- * (c) 2020 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
- * (https://dragonwocky.me/notion-enhancer) under the MIT license
- */
-
-'use strict';
-
-module.exports = function (target, __exports) {
-  console.log(target);
-};
diff --git a/insert/media b/insert/media
index 0e56fb9..5472e23 160000
--- a/insert/media
+++ b/insert/media
@@ -1 +1 @@
-Subproject commit 0e56fb9242a00e41132b9ad30adef9ae910a2159
+Subproject commit 5472e23e983ab893b72af6493bbf582017c182be
diff --git a/insert/repo b/insert/repo
index 4c589ec..bede50b 160000
--- a/insert/repo
+++ b/insert/repo
@@ -1 +1 @@
-Subproject commit 4c589ec5915cccfb004098caf08e8934eb73ade7
+Subproject commit bede50bedaacfe198de2e9a0015b454295dbb1e4
diff --git a/package.json b/package.json
index 2b0a70f..9c8033d 100644
--- a/package.json
+++ b/package.json
@@ -3,11 +3,12 @@
   "version": "0.11.0-dev",
   "author": "dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)",
   "description": "an enhancer/customiser for the all-in-one productivity workspace notion.so",
-  "homepage": "https://github.com/notion-enhancer/notion-enhancer",
+  "homepage": "https://github.com/notion-enhancer/desktop",
   "license": "MIT",
   "bin": {
     "notion-enhancer": "bin.mjs"
   },
+  "private": true,
   "type": "module",
   "engines": {
     "node": ">=16.x.x"
diff --git a/pkg/apply.mjs b/pkg/apply.mjs
index 3492b78..9293684 100644
--- a/pkg/apply.mjs
+++ b/pkg/apply.mjs
@@ -17,7 +17,7 @@ import remove from './remove.mjs';
 
 export default async function (
   notionFolder = findNotion(),
-  { overwritePrevious = undefined, takeBackup = true } = {}
+  { overwritePrevious = undefined, takeBackup = true, applyDevPatch = false } = {}
 ) {
   let status = check(notionFolder);
   switch (status.code) {
@@ -26,8 +26,11 @@ export default async function (
     case 1: // corrupted
       throw Error(status.message);
     case 2: // same version already applied
-      log`  {grey * notion-enhancer v${status.version} already applied}`;
-      return true;
+      if (!applyDevPatch) {
+        log`  {grey * notion-enhancer v${status.version} already applied}`;
+        return true;
+      }
+      break;
     case 3: // diff version already applied
       log`  * ${status.message}`;
       const prompt = ['Y', 'y', 'N', 'n', ''],
@@ -60,31 +63,33 @@ export default async function (
   }
 
   s = spinner('  * inserting enhancements').loop();
-  const notionFiles = (await readDirDeep(status.executable))
-    .map((file) => file.path)
-    .filter((file) => file.endsWith('.js') && !file.includes('node_modules'));
-  for (const file of notionFiles) {
-    const target = file.slice(status.executable.length + 1, -3),
-      replacer = path.resolve(`${__dirname(import.meta)}/replacers/${target}.mjs`);
-    if (fs.existsSync(replacer)) {
-      await (await import(`./replacers/${target}.mjs`)).default(file);
+  if (!(status.code === 2 && applyDevPatch)) {
+    const notionFiles = (await readDirDeep(status.executable))
+      .map((file) => file.path)
+      .filter((file) => file.endsWith('.js') && !file.includes('node_modules'));
+    for (const file of notionFiles) {
+      const target = file.slice(status.executable.length + 1, -3),
+        replacer = path.resolve(`${__dirname(import.meta)}/replacers/${target}.mjs`);
+      if (fs.existsSync(replacer)) {
+        await (await import(`./replacers/${target}.mjs`)).default(file);
+      }
+      await fsp.appendFile(
+        file,
+        `\n\n//notion-enhancer\nrequire('notion-enhancer')('${target}', exports))`
+      );
     }
-    await fsp.appendFile(
-      file,
-      `\n\n//notion-enhancer\nrequire('notion-enhancer')('${target}', exports);`
-    );
   }
+  const node_modules = path.resolve(`${status.executable}/node_modules/notion-enhancer`);
+  await copyDir(`${__dirname(import.meta)}/../insert`, node_modules);
   s.stop();
 
   s = spinner('  * recording version').loop();
-  const node_modules = path.resolve(`${status.executable}/node_modules/notion-enhancer`);
-  await copyDir(`${__dirname(import.meta)}/../insert`, node_modules);
   await fsp.writeFile(
     path.resolve(`${node_modules}/package.json`),
     `{
       "name": "notion-enhancer",
       "version": "${pkg().version}",
-      "main": "launcher.js"
+      "main": "init.cjs"
     }`
   );
   s.stop();
diff --git a/pkg/remove.mjs b/pkg/remove.mjs
index 6fcfa35..561593d 100644
--- a/pkg/remove.mjs
+++ b/pkg/remove.mjs
@@ -43,4 +43,6 @@ export default async function (notionFolder = findNotion(), { delCache = undefin
       s.stop();
     }
   } else log`  {grey * enhancer cache not found: skipping}`;
+
+  return true;
 }
diff --git a/pkg/replacers/main/main.mjs b/pkg/replacers/main/main.mjs
index f8b6fe6..2edfa6b 100644
--- a/pkg/replacers/main/main.mjs
+++ b/pkg/replacers/main/main.mjs
@@ -9,7 +9,7 @@
 import fsp from 'fs/promises';
 
 export default async function (filepath) {
-  // https://github.com/notion-enhancer/notion-enhancer/issues/160
+  // https://github.com/notion-enhancer/desktop/issues/160
   // enable the notion:// url scheme/protocol on linux
   const contents = await fsp.readFile(filepath, 'utf8');
   await fsp.writeFile(
diff --git a/pkg/replacers/main/schemeHandler.mjs b/pkg/replacers/main/schemeHandler.mjs
new file mode 100644
index 0000000..5f21cfe
--- /dev/null
+++ b/pkg/replacers/main/schemeHandler.mjs
@@ -0,0 +1,43 @@
+/*
+ * notion-enhancer
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+import fsp from 'fs/promises';
+
+export default async function (filepath) {
+  // https://github.com/notion-enhancer/desktop/issues/291
+  // bypass csp issues by intercepting notion:// protocol
+  const contents = await fsp.readFile(filepath, 'utf8');
+  await fsp.writeFile(
+    filepath,
+    contents.replace(
+      /const success = protocol\.registerStreamProtocol\(config_1.default.protocol, async \(req, callback\) => \{/,
+      `const success = protocol.registerStreamProtocol(config_1.default.protocol, async (req, callback) => {
+      {
+        // notion-enhancer
+        const schemePrefix = 'notion://www.notion.so/__notion-enhancer/';
+        if (req.url.startsWith(schemePrefix)) {
+          const resolvePath = (path) => require('path').resolve(\`\${__dirname}/\${path}\`),
+            fileExt = req.url.split('.').reverse()[0],
+            filePath = resolvePath(
+              \`../node_modules/notion-enhancer/\${req.url.slice(schemePrefix.length)}\`
+            ),
+            mimeDB = Object.entries(require('notion-enhancer/dep/mime-db.json')),
+            mimeType = mimeDB
+              .filter(([mime, data]) => data.extensions)
+              .find(([mime, data]) => data.extensions.includes(fileExt));
+          callback({
+            data: require('fs').createReadStream(filePath),
+            headers: { 'content-type': mimeType },
+          });
+        }
+      }`
+    )
+  );
+
+  return true;
+}