From 02344d169c1bfaca07bf0f26a67a5cde429e1f61 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Fri, 13 Mar 2026 03:16:07 +0100
Subject: [PATCH] feat: expand addon system documentation with rebuild
requirements and runtime loading strategy
---
specifications/addon-plugin-theme-concept.md | 64 +++++++++++++++++++-
1 file changed, 63 insertions(+), 1 deletion(-)
diff --git a/specifications/addon-plugin-theme-concept.md b/specifications/addon-plugin-theme-concept.md
index 801ee8c9..eaf457ad 100644
--- a/specifications/addon-plugin-theme-concept.md
+++ b/specifications/addon-plugin-theme-concept.md
@@ -34,6 +34,18 @@ This document describes a system that allows the JMAP Webmail application to be
- **Aligned with existing architecture** — builds on Zustand stores, React context/providers, CSS variables, and the Next.js App Router patterns already in use.
- **Incrementally adoptable** — the core app can ship without any addons; the addon system is a layer on top.
+### Rebuild Requirements
+
+| Addon Type | Source | Rebuild Needed? |
+|-----------|--------|----------------|
+| Theme | Bundled (`/addons/themes/`) | **Yes** — included at build time |
+| Theme | URL (remote) | **No** — CSS loaded via `` at runtime |
+| Plugin | Bundled (`/addons/plugins/`) | **Yes** — included at build time |
+| Plugin | URL (remote) | **No** — JS loaded at runtime (see §6.3) |
+| Plugin | Local file (dev mode) | **No** — loaded at runtime |
+
+Bundled addons are compiled into the app's static assets during `next build`. Adding, removing, or updating a bundled addon requires a rebuild and redeploy. URL-based and local addons are fully runtime-loaded — users can install, enable, disable, and uninstall them without any rebuild.
+
### Non-Goals (for v1)
- Server-side plugin execution (all addons run client-side).
@@ -389,6 +401,54 @@ interface Disposable {
5. **Activate**: Call `activate(ctx)` for each plugin, passing a scoped `PluginContext`.
6. **Ready**: Emit `app:ready` hook — plugins can now interact with stores.
+### 6.3 Runtime Loading Strategy
+
+Next.js `import()` only resolves modules known at build time. To load plugins from arbitrary URLs at runtime without a rebuild, the addon system uses a **script-based module loader**:
+
+```ts
+// lib/addon-loader.ts
+
+async function loadRemotePlugin(url: string): Promise {
+ // 1. Fetch the plugin's JS bundle as text
+ const response = await fetch(url);
+ if (!response.ok) throw new Error(`Failed to fetch plugin: ${response.status}`);
+ const code = await response.text();
+
+ // 2. Validate size limit (500 KB default)
+ if (code.length > MAX_PLUGIN_SIZE) {
+ throw new Error(`Plugin exceeds size limit`);
+ }
+
+ // 3. Create a scoped module environment
+ // The plugin receives a controlled `require` that only resolves
+ // allowed shared dependencies (React, Lucide icons, date-fns).
+ const module = { exports: {} as PluginModule };
+ const scopedRequire = createScopedRequire(SHARED_DEPS);
+ const factory = new Function("module", "exports", "require", code);
+ factory(module, module.exports, scopedRequire);
+
+ // 4. Validate the module exports the expected interface
+ if (typeof module.exports.activate !== "function") {
+ throw new Error(`Plugin does not export an activate() function`);
+ }
+
+ return module.exports;
+}
+
+// Shared dependencies exposed to plugins — avoids bundling duplicates
+const SHARED_DEPS: Record = {
+ "react": React,
+ "react/jsx-runtime": jsxRuntime,
+ "lucide-react": lucideIcons,
+ "date-fns": dateFns,
+ "sonner": sonner,
+};
+```
+
+**Bundled addons** skip this loader — they are statically imported at build time via the generated `addon-registry.json`.
+
+**Theme CSS** (any source) is loaded by injecting a `` element — no special loader needed.
+
### 6.2 Addon Manager Store
A new Zustand store manages addon state:
@@ -934,4 +994,6 @@ export function activate(ctx: PluginContext) {
4. **Addon signing?** For URL-installed addons, a signature verification system would prevent tampering. Worth considering for v2.
-5. **Shared dependencies?** Should plugins be able to declare peer dependencies on the host app's packages (React, date-fns, Lucide icons)? This would reduce bundle sizes but creates coupling. Recommend providing these as globals via the plugin runtime.
+5. **Shared dependencies?** Should plugins be able to declare peer dependencies on the host app's packages (React, date-fns, Lucide icons)? This would reduce bundle sizes but creates coupling. Recommend providing these as globals via the plugin runtime. **(Addressed in §6.3 — shared deps are exposed via a scoped `require`.)**
+
+6. **Admin-controlled addon allowlist?** In multi-user deployments, should the server admin be able to restrict which addons can be installed (e.g., via an environment variable `ALLOWED_ADDON_IDS`)? This would prevent users from loading untrusted plugins in managed environments.