Skip to main content
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

  1. Eight packages, each answering one question. Pick the package by the question the code is asking:
  2. Events is the bus for everything. @OnEvent handles 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 no pubsub package.
  3. 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 ./contracts entry (0117), leave through the outbox and arrive through the inbox.
  4. Outbox is its own runtime package, because it needs MikroORM and a database, and @stackra/support stays 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.
  5. Queue holds jobs, one-off delayed and run-at jobs, retries and a dead-letter queue. Drivers: Encore topic, Postgres SKIP LOCKED, memory.
  6. Scheduler is recurring cron only. In SaaS it runs on Encore CronJob. On-prem and air-gapped it runs on a croner timer with a Postgres advisory lock. A scheduler handler only dispatches a job or a workflow; it does no work itself.
  7. 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”).
  8. Packages never import encore.dev. stackra gen (0091) emits Topic, Subscription and CronJob declarations for the service and passes them to duck-typed drivers.
  9. 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/webhooks follows 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.

Supersedes and sources

Supersedes: 0033 (its outbox, idempotent-consumer and versioned-name rules are restated in decisions 3 and 4; Encore Pub/Sub and cron become the SaaS drivers). 0043 is superseded by 0102. Sources: owner decisions in chat, 2026-10-19 (Round 6 items 17 and 19; Round 8 “messaging layer locked”, F5).