Event model
Flightdeck’s events are in-process, today — internal events never cross
a network. EventBridge/SQS exist only inside edges/ (see
Topology). The contract shape is deliberately the same
shape an out-of-process bus would use, so promoting a module to its own
service later doesn’t require redesigning its events.
Registration — one line, impossible to half-register
Every event is defined once in contracts/src/events/<domain>.ts and rolled
up into a single central registry, EVENTS, in
contracts/src/events/index.ts. If an event name isn’t in EVENTS, it
cannot be published and cannot be subscribed to — the registry is the
whole contract surface, checked at both publish and subscribe time.
Schema evolution is additive within a version; a breaking payload
change ships as a new .v2 definition. A shipped payload is never mutated
in place.
The envelope
Every event carries a tenant_ref — required, not optional — and a
payload that is self-contained: a consumer must never need to call back
into the producer to make use of an event. This is what lets consumers stay
decoupled even though dispatch is in-process today.
The dispatcher
app/src/dispatcher.ts is a small in-process pub/sub:
- Modules register consumers at boot (
dispatcher.register(moduleName, handlers)); registering after boot throws — the dispatcher is sealed once startup completes. - Subscribing to an event name that isn’t in the central
EVENTSregistry throws immediately, at boot — not at first dispatch. - Publishing an unregistered event name throws.
- Consumers run after the publishing transaction. A consumer failure is logged and surfaced — never silently swallowed.
Built-must-be-wired, applied to events
A registered event with zero consumers, or a module that calls
publish on an event nobody imports, is exactly the kind of orphan the
conventions checker looks for. Registering an event is not the same as
shipping it — the consumer’s infra.ts/dispatcher registration is the step
that actually makes it fire.
Adding a new event — the four steps
- Define the payload type in
contracts/src/events/<domain>.ts. - Add its line to the central
EVENTSobject incontracts/src/events/index.ts. - Publish it from the owning module via the injected
bus.publish. - Wire the consumer(s) — a module registers its handler with the dispatcher at construction time. A registered-but-unconsumed event is an orphan; a consumer with no upstream publisher is a dead scaffold. Both fail the conventions check.