feat: add native module realtime transport
CI / verify (pull_request) Successful in 37s
CI / infrastructure-smoke (pull_request) Successful in 13s
CI / migration-smoke (pull_request) Successful in 1m7s

This commit is contained in:
ekklesia
2026-08-23 18:47:18 -05:00
parent fcc531a39c
commit fc6c8045c4
53 changed files with 3959 additions and 108 deletions
+42 -11
View File
@@ -12,9 +12,9 @@ transport), and Phase 5C (module database access) have landed. **Phase 5D (secur
against an independent security audit) is complete: all five passes have landed and the final
independent whole-repository re-audit has closed, under the current trusted-contributor,
not-yet-deployed threat model — and Phase 6 is unblocked and underway.** Phase 6 has since
completed through 6D, and **Phase 7A passes 1 (the minimal operational module lifecycle), 2 (the module configuration and
secrets contract), 3 (host-provided module object storage), and 4 (direct browser object delivery)
have landed.** The operational
completed through 6D, and **Phase 7A passes 1 (the minimal operational module lifecycle), 2 (the
module configuration and secrets contract), 3 (host-provided module object storage), 4 (direct
browser object delivery), and 5 (native module realtime transport) have landed.** The operational
requirements that remain are prerequisites for two specific *future* activities — executing
untrusted CI contributions, and a public production launch — and neither gates Phase 6; see the
pass 5 closure below and
@@ -340,15 +340,17 @@ GET /api/v1/module-ui → which module UIs may be mounted, and where
@ekklesia/module-ui-host → dynamic import, identity check, mount into a host-owned container
↓
context.api.request("records") → /api/v1/modules/example/records
context.realtime.subscribe("state") → host-owned live state transport
```
The new inventory endpoint returns exactly what building a `ModuleUiHostContext` needs — id, version,
artifact URL, module API root, and the viewer's granted permissions **for that module** — and nothing
else: no filesystem paths, and no `name`/`icon`/`folder`/`order`, because navigation metadata is a
design that does not exist yet. It is authenticated (`core.modules.ui.read`, baseline
The inventory endpoint returns exactly what building a `ModuleUiHostContext` needs — id, version,
artifact URL, module API root, module realtime root, and the viewer's granted permissions **for that
module** — and nothing else: no filesystem paths, and no `name`/`icon`/`folder`/`order`, because
navigation metadata is a separate design. It is authenticated (`core.modules.ui.read`, baseline
`authenticated`) precisely because it is the module enumeration the anonymous artifact route
deliberately withholds. Both URLs are produced by the server's own canonical helpers, so the browser
never rebuilds Ekklesia's module prefixes and either can change without touching host or module code.
deliberately withholds. All three URLs are produced by the server's own canonical helpers, so the
browser never rebuilds Ekklesia's module prefixes and any can change without touching host or module
code.
**The shell statically imports no module.** Every module it can mount arrives at run time from the
inventory, and its bundle is checked for exactly one dynamic import — the generic loader's — with no
@@ -594,10 +596,39 @@ and rewritten to another. The origin a browser uses is therefore a signing input
(`S3_DELIVERY_ENDPOINT`), defaulting to `S3_ENDPOINT` — so the standard deployment configures nothing
and delivery simply works. A reverse proxy in front of storage must pass the original `Host` through
unchanged; it may terminate TLS, because the scheme is not signed. Buckets stay private: an unsigned
request for the same object is refused, and a test asserts it. **Signed write and delete URLs, and a
realtime refresh transport, are deliberately deferred.** See
request for the same object is refused, and a test asserts it. **Signed write and delete URLs are
deliberately deferred.** See
[ADR 0031](docs/architecture/decisions/0031-direct-object-delivery.md).
Phase 7A pass 5 adds native, host-neutral module realtime
([ADR 0032](docs/architecture/decisions/0032-native-module-realtime-transport.md)). During
`register`, a module declares a small, module-local channel and the permission that protects it;
during `start`, running work, and HTTP handlers, it publishes bounded JSON state through its own
scoped publisher. The publisher is inert by the time that module's stop hook begins. Core keeps only
the latest state and one pending update per subscriber. A module
must publish an initial state before startup completes, so every accepted subscriber receives one
atomic authoritative snapshot and then monotonically sequenced updates without a GET-then-subscribe
race. Slow clients never block publishers and intermediate states may be coalesced.
Ekklesia carries that contract over permission-gated Server-Sent Events, with cookie authentication,
15-second transport-owned keepalives, a five-minute reauthorization cycle, browser-native reconnect,
and a fresh snapshot on every reconnection. The module UI API remains transport-neutral:
```ts
const subscription = context.realtime.subscribe<SlideshowState>("state", {
onSnapshot: (state) => render(state),
onUpdate: (state) => render(state),
});
// Return this from `mount`, or call it from your unmount cleanup.
subscription.close();
```
There is no durable replay, global topic namespace, cross-module subscription, query-string token,
Socket.IO contract, or MQTT integration. Direct object-delivery URLs are opaque JSON strings and
pass through realtime unchanged. The final unauthenticated/device principal for public displays and
display/kiosk hosting itself remain a later pass.
**Still not the finished Dashboard.** There is no navigation editor or drag and drop, no nested
folders, no icons or colors, no Dashboard Home or widgets, no design system, no client-side routing,
and no module-management, settings, or storage UI — module enablement and configuration are an API and