Amended by
0148:
decisions 5 and 6 gain the queue and scheduler features.
Amended by 0141: event names carry no service
prefix.
Context
Events, pub/sub, outbox, jobs, cron, workflows, realtime, webhooks and notifications overlap. Without boundaries, each gets reimplemented in the wrong place. 0033 tied events and cron directly to Encore Pub/Sub and Encore cron, which does not hold on-prem or air-gapped (0076). The owner locked the layer as eight packages, with no separate pub/sub package.Decision
-
Eight packages, each answering one question. Pick the package by the question the code is asking:
-
Events is the bus for everything.
@OnEventhandles every source the same way: in-process events, pub/sub between services, inbound webhooks, and client transports (SSE, WebSocket,BroadcastChannel, push). Sources and sinks are drivers. There is nopubsubpackage. -
Domain events and integration events are different. Domain events stay in-process and are not versioned. Integration events cross a service boundary. They are versioned by name (
user.created.v1), carry shapes from the producer’s./contractsentry (0117), leave through the outbox and arrive through the inbox. -
Outbox is its own runtime package, because it needs MikroORM and a database, and
@stackra/supportstays dependency-free. It owns the outbox table, the relay and inbox deduplication. The event row is written in the same transaction as the state change. Every consumer is idempotent and deduplicates on the event id. -
Queue holds jobs, one-off delayed and run-at jobs, retries and a dead-letter queue. Drivers: Encore topic, Postgres
SKIP LOCKED, memory. -
Scheduler is recurring cron only. In SaaS it runs on Encore
CronJob. On-prem and air-gapped it runs on acronertimer with a Postgres advisory lock. A scheduler handler only dispatches a job or a workflow; it does no work itself. - Workflow is steps plus compensation. Steps run in-process with a Postgres journal; only asynchronous, background or delayed steps go through the queue (0102, which replaced the first wording here, “each step runs as a queue job”).
-
Packages never import
encore.dev.stackra gen(0091) emitsTopic,SubscriptionandCronJobdeclarations for the service and passes them to duck-typed drivers. - Package placement: events in foundation; outbox, queue, scheduler, workflow and realtime in runtime; webhooks and notifications in communication.
Consequences
- A reviewer can reject, for example, cron work done inside a scheduler handler or a cross-service event published without the outbox.
- Every package has a SaaS driver and a self-hosted driver, and so carries two code paths.
- An outbox table, a relay and an inbox are a standing chore for every publishing service.
@stackra/webhooksfollows Standard Webhooks (owner decision, Round 6). Its own design ADR is written when the package lands (0018).
Enforcement
packages/tooling/oxlint/src/boundaries.jsonc bans encore.dev in packages. Planned: a conformance suite per driver package (queue, scheduler, events sources and sinks, outbox relay), including “redelivery is a no-op”. A validator check that integration-event names end in .v<n>, and a lint rule that a scheduler handler only dispatches a job or a workflow.