Skip to content

Commit 5ca6194

Browse files
plugins page: the loader shipped (0.0.215)
Lead with createServer({ plugins }) and the activate(api) contract; appPaths/getAgent/raw-body move to 'the machinery underneath'. Status table gains the loader and ws.route rows; bridge joins Tideholm as the second reference plugin (the realtime one, with the WebSocket ticket pattern); road-ahead reduces to CLI config block, #564 migration, and consumer-demanded seams.
1 parent 6647b34 commit 5ca6194

1 file changed

Lines changed: 104 additions & 37 deletions

File tree

docs/features/plugins.md

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,81 @@ description: Extend JSS with whole applications — where the plugin system stan
88

99
JSS can host entire applications beside your pod — same origin, same server,
1010
with 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
3895
identity.
3996

4097
The 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) => {
137194
proposes packaging this as `api.mountApp(prefix, nodeHandler)` so nobody
138195
rediscovers 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

164232
From [#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.
174241
3. **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

180246
The pattern for contributing a seam is established: build a real thing
181247
against 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

Comments
 (0)