@@ -8,7 +8,7 @@ description: Extend JSS with whole applications — where the plugin system stan
88
99JSS can host entire applications beside your pod — same origin, same server,
1010with the pod's identity system as the app's login. This page covers what
11- works ** today** (v0.0.215 +), how to build a plugin, and the seams underneath.
11+ works ** today** (v0.0.219 +), how to build a plugin, and the seams underneath.
1212
1313:::tip The one-line version
1414` createServer({ plugins: [{ module: 'my-app/plugin.js', prefix: '/myapp' }] }) `
@@ -21,13 +21,17 @@ be the app's account — no separate passwords.
2121
2222| Piece | Status | Where |
2323| ---| ---| ---|
24- | ** Plugin loader** (` plugins ` option, ` activate(api) ` ) | ✅ ** Shipped in v0.0.215** | [ #206 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 ) , [ #589 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/589 ) |
25- | Application mount points (` appPaths ` ) | ✅ Shipped in v0.0.213 | [ #582 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/582 ) , [ #585 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/585 ) |
26- | Public identity accessor (` getAgent ` ) | ✅ Shipped in v0.0.214 | [ #584 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/584 ) , [ #586 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/586 ) |
27- | WebSocket routing for realtime plugins (` api.ws.route ` ) | ✅ Shipped in v0.0.215 | [ #588 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/588 ) , [ #589 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/589 ) |
28- | Reference plugins | ✅ Two running — [ Tideholm] ( https://github.com/melvincarvalho/tideholm/tree/gh-pages/jss-plugin ) (strategy game, pod WebIDs as player accounts) and [ bridge] ( https://github.com/melvincarvalho/bridge/tree/gh-pages/jss-plugin ) (realtime card game over WebSocket) | [ #206 discussion] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 ) |
29- | Raw-body mode for wrapped apps | 📋 Pattern documented below; helper proposed | [ #583 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583 ) |
30- | Bundled-feature migration, CLI config block, panes | 🔭 Next | [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) |
24+ | ** Plugin loader** (` plugins ` option, ` activate(api) ` ) | ✅ ** Shipped v0.0.215** | [ #206 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 ) , [ #589 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/589 ) |
25+ | Application mount points (` appPaths ` ) | ✅ Shipped v0.0.213 | [ #582 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/582 ) , [ #585 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/585 ) |
26+ | Public identity accessor (` getAgent ` ) | ✅ Shipped v0.0.214 | [ #584 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/584 ) , [ #586 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/586 ) |
27+ | WebSocket routing (` api.ws.route ` ) | ✅ Shipped v0.0.215 | [ #588 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/588 ) , [ #589 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/589 ) |
28+ | CLI: config-file ` plugins ` + repeatable ` --plugin module@prefix ` | ✅ Shipped v0.0.216–217 | [ #593 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/593 ) , [ #595 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/595 ) |
29+ | Wrapping node apps (` api.mountApp ` ) | ✅ Shipped v0.0.217 | [ #583 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583 ) , [ #590 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/590 ) |
30+ | Server origin (` api.serverInfo() ` ) | ✅ Shipped v0.0.218 | [ #601 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/601 ) , [ #605 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/605 ) |
31+ | Reserve fixed paths (` api.reservePath ` ) | ✅ Shipped v0.0.218 | [ #602 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/602 ) , [ #607 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/607 ) |
32+ | Loaded-plugin roster (` api.plugins ` ) | ✅ Shipped v0.0.218 | [ #610 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/610 ) , [ #612 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/612 ) |
33+ | Reference plugins | ✅ A [ 33-plugin out-of-tree suite] ( https://github.com/JavaScriptSolidServer/plugins ) (dashboard, WebDAV/CalDAV/CardDAV, Mastodon/Bluesky/Matrix shims, S3, JMAP, …) plus [ Tideholm] ( https://github.com/melvincarvalho/tideholm ) & [ bridge] ( https://github.com/melvincarvalho/bridge ) | [ plugins repo] ( https://github.com/JavaScriptSolidServer/plugins ) |
34+ | Bundled-feature migration; ` api.events ` , ` api.authorize ` seams; panes | 🔭 Next | [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) , [ #603 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/603 ) , [ #604 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/604 ) |
3135
3236## Using the loader
3337
@@ -49,6 +53,16 @@ const fastify = createServer({
4953await fastify .listen ({ port: 4443 });
5054```
5155
56+ No config file? Mount the same apps from the CLI — the repeatable ` --plugin `
57+ flag takes ` module[@prefix] ` and appends to any config-file ` plugins `
58+ (v0.0.216–217):
59+
60+ ``` bash
61+ jss start \
62+ --plugin ./relay/plugin.js@/relay \
63+ --plugin ./dashboard/plugin.js@/dashboard
64+ ```
65+
5266A plugin module exports one function:
5367
5468``` js
@@ -67,15 +81,33 @@ export async function activate(api) {
6781 // Private server-side storage (never served over HTTP):
6882 const dir = api .storage .pluginDir ();
6983
84+ // The server's own origin — for absolute URLs and loopback calls, so you
85+ // never repeat baseUrl in config. Call it per request (with port 0 the
86+ // real port exists only once listening):
87+ const { baseUrl , host , port } = api .serverInfo (); // (#601)
88+
89+ // Every co-loaded plugin, read-only — enumerate your siblings instead of a
90+ // hand-copied list (this is how the dashboard plugin knows what to show):
91+ for (const { id , prefix , module } of api .plugins ) { /* … */ } // (#610)
92+
7093 return { deactivate () { /* save state, clear timers */ } };
7194}
7295```
7396
74- The api also carries ` api.config ` (the entry's config, verbatim) and
75- ` api.log ` (speaks both pino and console dialects). A plugin that fails to
76- import or activate ** fails ` listen() ` loudly** — a server silently missing
77- an app is worse than one that refuses to start. Entries take an optional
78- ` id ` when two modules would reduce to the same name (it keys ` pluginDir ` ).
97+ The api also carries ` api.config ` (the entry's config, verbatim), ` api.log `
98+ (speaks both pino and console dialects), ` api.reservePath(path) ` — claim and
99+ WAC-exempt a fixed or ` /:param/ ` -shaped route ** outside** your prefix, for
100+ protocols that pin absolute paths (` /xrpc ` , ` /:user/did.json ` ) — and
101+ ` api.mountApp(handler, { prefix }) ` for wrapping a node-style app (below). A
102+ plugin that fails to import or activate ** fails ` listen() ` loudly** — a
103+ server silently missing an app is worse than one that refuses to start.
104+
105+ Entries take an optional ` id ` that keys ` pluginDir ` . It defaults to a name
106+ derived from ` module ` : the file's basename, or its ** parent directory** for
107+ the conventional ` <name>/plugin.js ` layout (` relay/plugin.js ` → ` relay ` ), so
108+ distinct plugins get distinct ids with none set (v0.0.219+,
109+ [ #596 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/596 ) ).
110+ Set it explicitly only if two specifiers still reduce to the same name.
79111
80112Plugins load ** only from operator config — never from pod storage** (pods
81113are user-writable; a loader that read them would be remote code execution).
@@ -165,39 +197,46 @@ endpoint, stores the returned Bearer token, and attaches it to the app's
165197API calls. Tokens expire after 3600s — handle the 401 by returning to the
166198login screen.
167199
168- ## Wrapping an existing app (raw bodies )
200+ ## Wrapping an existing app (` api.mountApp ` )
169201
170202If your "plugin" is an existing node-style HTTP app (` (req, res) ` handler),
171- two gotchas — both solved with one pattern:
172-
173- 1 . Fastify's content parsers ** consume request bodies ** before handlers
174- run, so your wrapped app hangs waiting for a stream that's been drained.
175- 2 . Fastify wants to own the response unless you tell it otherwise.
203+ two gotchas bite: Fastify's content parsers ** consume request bodies **
204+ before handlers run (so a wrapped app hangs waiting for a stream that's been
205+ drained), and Fastify wants to own the response. ` api.mountApp ` (v0.0.217,
206+ [ # 590 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/pull/590 ) )
207+ handles both — plus the ` appPaths ` exemption and ` reply.hijack() ` :
176208
177209``` js
178- await fastify .register (async (scope ) => {
179- // Pass bodies through untouched, scoped so the host is unaffected.
180- scope .removeAllContentTypeParsers ();
181- scope .addContentTypeParser (' *' , (req , payload , done ) => done (null , payload));
182-
183- const handler = async (request , reply ) => {
184- request .raw .myAppAgent = await getAgent (request); // identity for the raw handler
185- reply .hijack (); // fastify lets go of the response
186- myNodeApp .handle (request .raw , reply .raw );
187- };
188- scope .all (' /myapp' , handler);
189- scope .all (' /myapp/*' , handler);
190- });
210+ export async function activate (api ) {
211+ await api .mountApp (
212+ (req , res ) => myNodeApp .handle (req, res), // your (req, res) app
213+ { prefix: api .prefix }, // omit to use the entry prefix
214+ );
215+ }
191216```
192217
193- [ #583 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583 )
194- proposes packaging this as ` api.mountApp(prefix, nodeHandler) ` so nobody
195- rediscovers it the hard way.
218+ The wrapped app receives the ** undrained** request stream and owns the
219+ response — including gzipped bodies, which it inflates itself. Need the
220+ caller's identity inside it? Resolve it before handing off:
221+ ` req.agent = await api.auth.getAgent(request) ` .
222+
223+ Under the hood it's a scoped pass-through content-type parser plus
224+ ` reply.hijack() ` , registered on both the bare prefix and its subtree — the
225+ pattern plugins rediscovered the hard way before the helper shipped
226+ ([ #583 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583 ) ).
196227
197228## Case studies: the plugins that built the system
198229
199230Every seam above was forced by a real consumer before it shipped — that's
200- the house method. Two reference plugins are live at
231+ the house method, and it scaled. A
232+ [ 33-plugin out-of-tree suite] ( https://github.com/JavaScriptSolidServer/plugins )
233+ now exercises the whole api — a status ** dashboard** that auto-discovers its
234+ siblings via ` api.plugins ` , the WebDAV/CalDAV/CardDAV family, Mastodon /
235+ Bluesky / Matrix / ActivityPub shims, an S3 gateway, JMAP mail, Micropub,
236+ remoteStorage, and more, all on one server from pure config. Each plugin's
237+ ` README ` records the seam it needed and the wall it hit — the findings that
238+ drove ` serverInfo ` , ` reservePath ` , and ` api.plugins ` upstream. The two games
239+ remain the original forcing consumers, live at
201240[ nostr.social/tideholm] ( https://nostr.social/tideholm/ ) and
202241[ nostr.social/bridge] ( https://nostr.social/bridge/ ) :
203242
@@ -232,19 +271,22 @@ Tideholm player and a bridge seat with a single sign-in.
232271From [ #206 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 )
233272/ [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) :
234273
235- 1 . ** A ` plugins ` block in the CLI config file** — the programmatic option
236- shipped; a config-file form makes "install an app = edit config" real
237- for non-programmatic deployments.
238- 2 . ** Migrate the bundled features** onto the loader (#564 ) — the relay,
274+ 1 . ** Migrate the bundled features** onto the loader (#564 ) — the relay,
239275 ActivityPub, git, pay and friends become battle-tested consumers, one at
240276 a time.
241- 3 . ** Richer seams as consumers demand them** — ` api.mountApp ` (#583 ),
242- ` registerMcpTool ` , ` registerPane ` , with the
277+ 2 . ** The next demanded seams** —
278+ [ ` api.events.onResourceChange ` ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/603 )
279+ (react to pod writes; seven consumers waiting) and
280+ [ ` api.authorize ` ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/604 )
281+ (ask WAC about authority the caller doesn't drive), then ` registerMcpTool `
282+ and ` registerPane ` , with the
243283 [ pane store] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/184 )
244284 as the eventual marketplace layer.
285+ 3 . ** Publish the plugin suite as a package** so
286+ ` --plugin @jss/plugins/dashboard@/dashboard ` loads by name, no clone.
245287
246288The pattern for contributing a seam is established: build a real thing
247289against JSS, hit a wall, file the smallest issue that removes it, prove it
248- with your consumer. Three seams (` appPaths ` , ` getAgent ` , ` ws.route ` ) and
249- the loader itself each went from idea to npm in about a day this way — the
250- door is open.
290+ with your consumer. Seven seams (` appPaths ` , ` getAgent ` , ` ws.route ` ,
291+ ` mountApp ` , ` serverInfo ` , ` reservePath ` , ` api.plugins ` ) and the loader
292+ itself each went from idea to npm this way — the door is open.
0 commit comments