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
+35 -1
View File
@@ -7,7 +7,9 @@ document's guarantees.
Related: [ADR 0006](../architecture/decisions/0006-versioned-capabilities-and-compiled-module-runtime.md)
(compiled module runtime), [ADR 0016](../architecture/decisions/0016-module-database-access.md)
(module database access), [ADR 0020](../architecture/decisions/0020-module-runtime-and-migration-execution-hardening.md)
(module routing/lifecycle and migration execution), and
(module routing/lifecycle and migration execution),
[ADR 0032](../architecture/decisions/0032-native-module-realtime-transport.md)
(native module realtime), and
[Module development](../module-development/README.md).
## Installed modules are trusted compiled backend code
@@ -44,6 +46,7 @@ cooperative. None of them constrains a module that is not:
| `ekklesia.module.json` manifest and `manifestVersion` | Declared compatibility and dependency ordering |
| Permission declarations and the composed registry | Which permissions *this* module's own routes may name, checked when Core mounts them |
| Route validation, overlap rejection, body limits | Deterministic, unambiguous HTTP routing and request bounds |
| Scoped realtime channels, publishers, and subscriptions | Correct module-local state wiring, permission enforcement, and finite fanout — not process isolation |
| `Object.freeze` on contexts and services | Prevents accidental mutation; a module can still reach the underlying objects |
| The `ModuleDatabase` facade | A typed, ergonomic surface — not a privilege reduction, since `execute` is arbitrary SQL |
| Capability registry | Explicit inter-module wiring, not access control |
@@ -316,6 +319,37 @@ see the URL it is fetching.
an unsigned request for the same object is refused, and an integration test asserts it so that a
future change cannot "fix" delivery by opening a bucket up.
### Realtime is permission-gated state delivery, not a trust boundary
A module may declare a local realtime channel protected by one of its own manifest permissions. Core
closes both the server publisher and browser subscription over that module id: neither API accepts a
caller-supplied module namespace, global topic, wildcard, broker address, origin, URL, permission, or
credential. This prevents accidental cross-module wiring by a cooperating module. It does not
sandbox hostile installed code; trusted same-process backend code could bypass the facade, and
trusted same-realm UI could call browser globals directly.
Ekklesia's SSE endpoint resolves the ordinary session cookie and evaluates the channel's fixed
permission before allocating a subscriber. There is no query-string token and no second realtime
authentication path. A stream is closed after five minutes so native EventSource reconnects and
session, grant-expiry, revocation, and baseline policy are evaluated again; authorization is not
continuously queried between those boundaries. Disabled and dependency-blocked modules have no
stream route at all. Public or device/display identity remains deliberately unsolved, so no channel
is made anonymous merely to support a future kiosk.
Fanout is finite: 16 channels per module, 100 subscribers per channel, 64 KiB of validated serialized
JSON per publication, one latest snapshot per channel, and one coalescing pending slot per client.
A slow browser cannot block a publisher or create an unbounded queue. The endpoint rejects implicit
`HEAD` before allocation, disconnect/cancel closes its Core subscription, and server shutdown closes
streams before finite HTTP drain. Each scoped publisher becomes inert immediately before its own
module's stop hook.
Core never adds configuration, secret values, database values, storage credentials, or delivery URLs
to a realtime document. It transports only what trusted module code explicitly publishes. A module
must therefore treat realtime like any other authorized response: include only data every principal
holding the channel permission may receive. If it intentionally includes a signed delivery URL, that
URL remains a bearer capability and is forwarded byte-for-byte; fixed transport errors never reflect
payloads, URLs, permissions, runtime exceptions, paths, or stacks.
### Client-side permission checks are not a security boundary
A host may tell a module UI what permissions it believes the viewer holds