@@ -8,24 +8,81 @@ 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.213+), how to build on it, and where the full plugin
12- system is headed.
11+ works ** today** (v0.0.215+), how to build a plugin, and the seams underneath.
1312
1413:::tip The one-line version
15- ` createServer({ appPaths: ['/myapp'] }) ` mounts an application under a URL
16- prefix. The app owns everything below its prefix; the pod keeps everything
17- else. Your WebID can be the app's account — no separate passwords.
14+ ` createServer({ plugins: [{ module: 'my-app/plugin.js', prefix: '/myapp' }] }) `
15+ loads an application and mounts it under a URL prefix. The app owns
16+ everything below its prefix; the pod keeps everything else. Your WebID can
17+ be the app's account — no separate passwords.
1818:::
1919
2020## Status at a glance
2121
2222| Piece | Status | Where |
2323| ---| ---| ---|
24- | 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 ) |
25- | Reference plugin ("plugin zero") | ✅ Running — [ Tideholm] ( https://github.com/melvincarvalho/tideholm/tree/gh-pages/jss-plugin ) , a multiplayer game where pod WebIDs are player accounts | [ #206 discussion] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 ) |
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 ) |
2629| Raw-body mode for wrapped apps | 📋 Pattern documented below; helper proposed | [ #583 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/583 ) |
27- | 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 ) |
28- | Plugin loader (manifest, discovery, policy) | 🔭 Designed, not yet built | [ #206 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 ) , [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) |
30+ | Bundled-feature migration, CLI config block, panes | 🔭 Next | [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) |
31+
32+ ## Using the loader
33+
34+ Declare your apps; the server imports, mounts, and tears them down:
35+
36+ ``` js
37+ import { createServer } from ' javascript-solid-server/src/server.js' ;
38+
39+ const fastify = createServer ({
40+ root: ' ./data/pods' ,
41+ idp: true ,
42+ idpIssuer: ' https://pod.example' ,
43+ plugins: [
44+ { module: ' tideholm/jss-plugin/tideholm-jss.js' , prefix: ' /tideholm' ,
45+ config: { bots: 8 } },
46+ { module: ' ./my-app/plugin.js' , prefix: ' /myapp' },
47+ ],
48+ });
49+ await fastify .listen ({ port: 4443 });
50+ ```
51+
52+ A plugin module exports one function:
53+
54+ ``` js
55+ export async function activate (api ) {
56+ // HTTP routes — the prefix is already WAC-exempt (appPaths, below):
57+ api .fastify .all (api .prefix + ' /api/*' , async (request , reply ) => {
58+ const agent = await api .auth .getAgent (request); // WebID | did:nostr | null
59+ // ...
60+ });
61+
62+ // Realtime — the loader owns the upgrade; you get the ready socket:
63+ await api .ws .route (api .prefix + ' /ws' , (socket , request ) => {
64+ socket .on (' message' , (data ) => socket .send (data));
65+ });
66+
67+ // Private server-side storage (never served over HTTP):
68+ const dir = api .storage .pluginDir ();
69+
70+ return { deactivate () { /* save state, clear timers */ } };
71+ }
72+ ```
73+
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 ` ).
79+
80+ Plugins load ** only from operator config — never from pod storage** (pods
81+ are user-writable; a loader that read them would be remote code execution).
82+
83+ Everything below this point is the machinery the loader assembles — worth
84+ knowing when you're debugging, composing by hand, or targeting a pre-0.0.215
85+ host.
2986
3087## Why plugins?
3188
@@ -38,10 +95,10 @@ anything that wants to live on your pod's origin and speak to your pod's
3895identity.
3996
4097The strategy is deliberate: ship the smallest seam, prove it with a real
41- consumer, bless the API afterward. The first consumer — * plugin zero* — is
42- [ Tideholm ] ( https://github.com/melvincarvalho/tideholm ) , a zero-dependency
43- island-strategy game. Mounting it surfaced exactly three missing seams, of
44- which only one required a core change. That one shipped in v0.0.213 .
98+ consumer, bless the API afterward. Tideholm ( * plugin zero* ) forced
99+ ` appPaths ` (v0.0.213) and ` getAgent ` (v0.0.214); bridge ( * plugin two * , the
100+ first realtime app) forced ` ws.route ` ; the loader that assembles them
101+ shipped in v0.0.215 with both games as its test consumers .
45102
46103## What shipped: ` appPaths `
47104
@@ -137,13 +194,17 @@ await fastify.register(async (scope) => {
137194proposes packaging this as ` api.mountApp(prefix, nodeHandler) ` so nobody
138195rediscovers it the hard way.
139196
140- ## Case study: plugin zero
197+ ## Case studies: the plugins that built the system
141198
142- [ Tideholm] ( https://github.com/melvincarvalho/tideholm ) is the working
143- reference for everything above — a multiplayer browser strategy game whose
144- core is zero-dependency and transport-agnostic, with a ~ 100-line adapter
145- ([ ` jss-plugin/ ` ] ( https://github.com/melvincarvalho/tideholm/tree/gh-pages/jss-plugin ) )
146- that mounts it at ` /tideholm ` :
199+ Every seam above was forced by a real consumer before it shipped — that's
200+ the house method. Two reference plugins are live at
201+ [ nostr.social/tideholm] ( https://nostr.social/tideholm/ ) and
202+ [ nostr.social/bridge] ( https://nostr.social/bridge/ ) :
203+
204+ ** [ Tideholm] ( https://github.com/melvincarvalho/tideholm ) ** (plugin zero —
205+ forced ` appPaths ` and ` getAgent ` ): a multiplayer island-strategy game whose
206+ core is zero-dependency and transport-agnostic, with a ~ 100-line adapter at
207+ [ ` jss-plugin/ ` ] ( https://github.com/melvincarvalho/tideholm/tree/gh-pages/jss-plugin ) .
147208
148209- ** WebID → player** : first authenticated request auto-provisions a game
149210 account keyed to the pod identity. Two pods, two players. No passwords.
@@ -152,32 +213,38 @@ that mounts it at `/tideholm`:
152213- ** Dual mode** : the same game runs standalone (` node server.js ` , its own
153214 password accounts) or mounted (pod identity), switching via a tiny
154215 ` GET /api/meta ` the client reads at boot.
155- - ** Try it** : ` JSS_PATH=... node jss-plugin/demo.js ` runs a 12-check
156- end-to-end proof; ` jss-plugin/serve.js ` is a persistent composed server.
157216
158- The adapter also ships ` jss.plugin.json ` and an ` activate(api) ` entry —
159- dead code today, but shaped exactly like the coming loader contract, so it
160- becomes a drop-in plugin the day the loader lands.
217+ ** [ bridge] ( https://github.com/melvincarvalho/bridge ) ** (plugin two — forced
218+ ` ws.route ` ): a realtime card game, no build step, WebSocket tables. It
219+ contributed the ** ticket pattern** for WebSocket auth: the client
220+ authenticates a normal HTTP request (` /bridge/auth/nip98 ` ), ` getAgent `
221+ verifies it — NIP-98 signature or pod Bearer, same call — and the app mints
222+ a one-use short-TTL ticket presented in the first socket message.
223+ Credentials never cross the upgrade, and the host never touches the app's
224+ socket protocol.
225+
226+ Both adapters export the ` activate(api) ` contract, so one server can run
227+ both games from six lines of ` plugins: ` config — one pod account is a
228+ Tideholm player and a bridge seat with a single sign-in.
161229
162230## The road ahead
163231
164232From [ #206 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/206 )
165- / [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) ,
166- in order:
167-
168- 1 . ** Loader** — ` jss.plugin.json ` manifests (` { id, version, entry } ` ),
169- ` activate(api) ` entry points, allow/deny policy, per-plugin config.
170- Plugins load ** only from operator config paths — never from pod
171- storage** (pods are user-writable; a loader that reads them is RCE).
172- 2 . ** Migrate the bundled features** onto the loader (#564 ) — eight
173- battle-tested consumers from day one.
233+ / [ #564 ] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/564 ) :
234+
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,
239+ ActivityPub, git, pay and friends become battle-tested consumers, one at
240+ a time.
1742413 . ** Richer seams as consumers demand them** — ` api.mountApp ` (#583 ),
175- ` registerMcpTool ` , ` registerPane ` (` getAgent ` already shipped: the
176- loader hands plugins the same function as ` api.auth.getAgent ` ), with the
242+ ` registerMcpTool ` , ` registerPane ` , with the
177243 [ pane store] ( https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/184 )
178244 as the eventual marketplace layer.
179245
180246The pattern for contributing a seam is established: build a real thing
181247against JSS, hit a wall, file the smallest issue that removes it, prove it
182- with your consumer. Plugin zero took two seams (` appPaths ` , ` getAgent ` ) from idea to npm in a
183- day each — the door is open.
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.
0 commit comments