Browse pages
Durable and Live Delivery
Committed changes reach the outside world through durable delivery that survives crashes and through live updates that reach open screens in seconds. This page states how the product stages after-commit effects in the outbox for a Worker to deliver and when it replaces polling with realtime transport. A team selects it for delivery that survives process failure or for updates a measured target requires live.
Stack: any Layer: infrastructure
Activation#
Activation scope: local.
Applicable specification kinds: use-case, journey.
A use case selects this extension when a committed change needs delivery that survives a process failure. That delivery can carry an integration event to another system, a command that advances a journey step, or a side effect on an outside provider. Selecting it also activates litespec/backend/architecture#declare-the-execution-host-for-work-that-outlives-a-request: the project declares the execution host that runs the dispatcher.
A use case or journey selects this extension for live updates after the team measures polling against the update target: the polling interval, the visible delay, the request cost, and the required update speed. The selection also names the accepted cost of disconnects and missed messages: how the client recovers when the connection drops or a notification never arrives.
Baseline Relationship#
This extension replaces litespec/backend/persistence#collect-events-without-a-public-unit-of-work.
This extension switches on no optional record type: integration events, settings, and runbooks belong to the core. The project writes an integration-event record for each integration event it delivers, setting records for the delivery bounds, and runbook records for recovery and rollback. Each pushed update is a notification record, and each resource bound lives in a setting record.
One-way notifications travel over server-sent events, which need no added package because the platform ships them.
Standards#
Stage After-Commit Effects in the Outbox#
ruleruleguidancelitespec/ext/outbox#stage-after-commit-effects-in-the-outboxA use case declaring after-commit emits or runs effects MUST stage each effect in the outbox in the same transaction as its changes.
Applies toartifact: command-handler
Why
After-commit timing promises the message follows the commit: staged in the same transaction, the change and its message share one fate. An emits effect is a domain event the aggregate raises, and persistence stages that event in the outbox in the same commit as the aggregate, as litespec/backend/application#emit-declared-integration-events-after-commit requires. An effect sent from memory instead would vanish on a crash between the commit and the send. litespec/core/use-cases#declare-every-effect-with-one-verb-and-its-timing states the effect verbs and their timing, and async effects dispatch without the transaction.
Checka reviewer answers
Reviewed forEach after-commit emits and runs effect stages in the outbox in the same transaction as its changes, with each domain event staged in the same commit as its aggregate.
Marked guidance: a reviewer answers this question.
Declare Published Facts as Integration Events#
ruleruleguidancelitespec/ext/outbox#declare-published-facts-as-integration-eventsEach emits effect MUST name an integration-event record carrying direction, payload, and trust.
Applies toartifact: record:integration-event
Why
A published fact without a record is a promise nobody versioned: readers depend on fields no page states, and a field change breaks them without a diff. The integration-event record states the direction, the payload with its classifications, and the trust readers place in it. The edges page states the declaration and the record.
Checka reviewer answers
Reviewed forEach emits effect names an integration-event record carrying direction, payload, and trust.
Marked guidance: a reviewer answers this question.
Record Durable Delivery Behavior#
ruleruleguidancelitespec/ext/outbox#record-durable-delivery-behaviorA durable-delivery specification MUST document delivery guarantee, downstream boundary, duplicate behavior, retry horizon, and recovery runbook.
Applies toartifact: any
Why
Delivery crosses a system boundary, so the team agrees up front on the guarantee, the boundary, the duplicate behavior, the retry horizon, and the runbook that recovers when delivery fails. Without that agreement, the writer assumes once-only delivery while the target assumes retries, and both discover the mismatch in production. The agreement lands before the first message does.
Checka reviewer answers
Reviewed forEach delivery specification documents guarantee, downstream boundary, duplicate behavior, retry horizon, and recovery runbook.
Marked guidance: a reviewer answers this question.
Exclude Manually Repeatable Reactions and Jobs#
ruleruleguidancelitespec/ext/outbox#exclude-manually-repeatable-reactions-and-jobsA project MUST NOT activate outbox for a reaction or job whose loss the operators accept and which they can repeat manually.
Applies toartifact: reaction, job
Why
Outbox storage, dispatch, retries, and replay exist to prevent loss; a reaction or job whose loss the operators accept and can repeat by hand needs none of that machinery. The reaction's or job's onFailure states its handling, and the use case page records the accepted gap.
Checka reviewer answers
Reviewed forReactions and jobs whose loss the operators accept and can repeat manually carry no outbox records.
Marked guidance: a reviewer answers this question.
Emit Excluded Reactions and Jobs After Commit#
ruleruleguidancelitespec/ext/outbox#emit-excluded-reactions-and-jobs-after-commitA reaction or job the outbox excludes MUST emit after its commit succeeds without staging an outbox record.
Applies toartifact: reaction, job
Why
Without an outbox record, nothing survives a crash between the commit and the emit, so the reaction or job emits immediately after its commit succeeds and accepts the gap. Emitting before the commit risks worse: it fires for a change the database then rolls back.
Checka reviewer answers
Reviewed forExcluded reactions and jobs emit after commit success with no staged outbox record.
Marked guidance: a reviewer answers this question.
Stage Messages with Business Work#
ruleruleguidancelitespec/ext/outbox#stage-messages-with-business-workA command post-handler MUST stage pending durable messages in the same scoped business session as its aggregate changes.
Applies toartifact: any
Why
The business change and its message share one fate: both persist or neither does. Staging the message in the same scoped business session as the aggregate changes puts both under one commit, so a crash can never leave an accepted change without its message or a message without its change.
Checka reviewer answers
Reviewed forPending durable messages stage in the same scoped session as their aggregate changes.
Marked guidance: a reviewer answers this question.
Serialize Durable Messages#
ruleruleguidancelitespec/ext/outbox#serialize-durable-messagesAn outbox writer MUST serialize each pending durable message into an outbox record.
Applies toartifact: any
Why
The command scope ends when the request ends, while dispatch happens later in a Worker that never saw the request. Serializing the message into an outbox record gives that Worker everything it needs after the scope is gone.
Checka reviewer answers
Reviewed forEach pending durable message lands serialized in an outbox record.
Marked guidance: a reviewer answers this question.
Store Required Message Fields#
ruleruleguidancelitespec/ext/outbox#store-required-message-fieldsAn outbox record MUST contain message identity, kind, stable type, schema version, time, payload, tenant, state, attempt, and next-attempt data.
Applies toartifact: any
Why
The dispatcher, the retry logic, and the operator each read the record: identity and type route it, the schema version selects its reader, time and tenant scope it, and state, attempt, and next-attempt data drive the retry. A field stored nowhere forces one of those readers to guess.
Checka reviewer answers
Reviewed forEach outbox record carries identity, kind, type, schema version, time, payload, tenant, state, attempt, and next-attempt data.
Marked guidance: a reviewer answers this question.
Store Failure and Completion Evidence#
ruleruleguidancelitespec/ext/outbox#store-failure-and-completion-evidenceAn outbox record MUST retain its last error summary and completion time.
Applies toartifact: any
Why
When delivery stalls, the operator opens the record first: the last error summary says why the previous attempt failed, and the completion time says which records finished. Without that evidence on the record, each investigation starts from scattered logs instead.
Checka reviewer answers
Reviewed forEach outbox record retains its last error summary and completion time.
Marked guidance: a reviewer answers this question.
Classify Message Kinds#
ruleruleguidancelitespec/ext/outbox#classify-message-kindsAn outbox record MUST name its message kind as integration event, journey-step command, or provider side effect, with an integration event citing its integration-event record.
Applies toartifact: record:integration-event
Why
An integration event, a journey-step command, and a provider side effect dispatch through different owners and transports, so the record names which one it holds. The integration event cites its integration-event record, so the staged message traces to the published fact it carries.
Checka reviewer answers
Reviewed forEach outbox record names integration event, journey-step command, or provider side effect, with integration events citing their integration-event record.
Marked guidance: a reviewer answers this question.
Reject Independent Outbox Commits#
ruleruleguidancelitespec/ext/outbox#reject-independent-outbox-commitsAn outbox writer MUST NOT use another connection or independent commit for required durable delivery.
Applies toartifact: any
Why
Two commits on two connections can split around a crash: the business change persists while the message rolls back, or the message persists for a change that never happened. Sharing one PostgreSQL database does not make separate transactions atomic, so the message joins the business transaction instead.
Checka reviewer answers
Reviewed forWriter sources stage through the business session with no separate connection or commit.
Marked guidance: a reviewer answers this question.
Dispatch Claimed Records in Worker#
ruleruleguidancelitespec/ext/outbox#dispatch-claimed-records-in-workerA Worker MUST claim pending outbox records, dispatch a bounded batch, and record success or retry information.
Applies toartifact: any
Why
Dispatch needs time, retries, and network calls that no request transaction can hold. The Worker claims pending records, dispatches a bounded batch, and records each outcome, so the request commits fast while delivery proceeds on its own schedule.
Checka reviewer answers
Reviewed forThe Worker claims pending records, dispatches bounded batches, and records each outcome.
Marked guidance: a reviewer answers this question.
Exclude Request-Transaction Dispatch#
ruleruleguidancelitespec/ext/outbox#exclude-request-transaction-dispatchWebApi MUST NOT dispatch a durable message inside the request transaction.
Applies toartifact: endpoint
Why
A network call inside the request transaction holds database locks while the outside world answers, and a slow target slows every request behind it. The request commits only to make the message eligible; the Worker performs the delivery later.
Checka reviewer answers
Reviewed forRequest paths commit eligibility only, with dispatch living in the Worker.
Marked guidance: a reviewer answers this question.
Define Claim Ownership#
ruleruleguidancelitespec/ext/outbox#define-claim-ownershipAn outbox claim MUST use a short transaction with the lease owner, lease expiry, and fencing value that litespec/ext/jobs#store-execution-leases defines.
Applies toartifact: any
Why
Several Workers poll the same store, so each dispatch attempt needs one owner: the lease owner, the lease expiry, and the fencing value name it. The short transaction takes the claim without holding locks into the delivery itself.
Checka reviewer answers
Reviewed forEach claim takes owner, expiry, and fencing value in a short transaction.
Marked guidance: a reviewer answers this question.
Release Claim Transactions Before Calls#
ruleruleguidancelitespec/ext/outbox#release-claim-transactions-before-callsA Worker MUST NOT hold an outbox claim transaction open during a network call.
Applies toartifact: any
Why
A network call to a slow target can last seconds or minutes. Holding the claim transaction open across that call pins database locks and connection slots, starving the Workers behind it; releasing the claim first lets the database serve other work while the call runs.
Checka reviewer answers
Reviewed forClaim transactions close before the Worker makes network calls.
Marked guidance: a reviewer answers this question.
Restrict Completion to Current Lease Owners#
ruleruleguidancelitespec/ext/outbox#restrict-completion-to-current-lease-ownersA Worker MUST update an outbox record only while it owns the matching lease and fencing value.
Applies toartifact: any
Why
A Worker that lost its lease, through a crash, a pause, or an expiry, can wake up after another Worker claimed the same record. Checking the lease and fencing value before each update stops the stale Worker from overwriting the current owner's result.
Checka reviewer answers
Reviewed forRecord updates succeed only under the current lease and fencing value.
Marked guidance: a reviewer answers this question.
Requeue Expired Claims#
ruleruleguidancelitespec/ext/outbox#requeue-expired-claimsAn outbox store MUST return expired claims to eligible work.
Applies toartifact: any
Why
A Worker can die between claiming a record and dispatching it. Returning expired claims to eligible work lets a live Worker pick them up, so one crash delays a message instead of losing it.
Checka reviewer answers
Reviewed forExpired claims return to eligible work for a live Worker.
Marked guidance: a reviewer answers this question.
Accept Repeated Delivery#
ruleruleguidancelitespec/ext/outbox#accept-repeated-deliveryAn event handler that receives an outbox delivery MUST handle the same message more than once.
Applies toartifact: reaction
Why
The target can accept a message while the Worker crashes before recording completion, and the requeued claim then dispatches it again. Delivery promises at-least-once, so every event handler handles the same message more than once without corrupting its state.
Checka reviewer answers
Reviewed forEach event handler keeps correct state when the same message arrives twice.
Marked guidance: a reviewer answers this question.
Identify Repeated Delivery#
ruleruleguidancelitespec/ext/outbox#identify-repeated-deliveryAn event handler that receives an outbox delivery MUST use message ID or a business idempotency key at its Command or side-effect boundary.
Applies toartifact: reaction
Why
Handling a repeat means recognizing it: the message ID or a business idempotency key gives the event handler a stable identity to check at its command or side-effect boundary. Without that key, the handler processes the repeat as new work and pays, notifies, or ships twice.
Checka reviewer answers
Reviewed forEach event handler checks message ID or business key at its Command or side-effect boundary.
Marked guidance: a reviewer answers this question.
Deliver Each Event to Each Handler Separately#
ruleruleguidancelitespec/ext/outbox#deliver-each-event-to-each-handler-separatelyThe outbox MUST deliver an event to each of its handlers as a separate delivery with its own retries and dead letter.
Applies toartifact: any
Why
One delivery for all handlers retries every handler when one fails. Each handler therefore gets its own delivery with its own retries and dead letter, and each delivery still ends in repeat-safe handling under litespec/ext/outbox#accept-repeated-delivery.
Example
A loan-borrowed event feeds a due-notice handler and a statistics handler. The notice delivery retries on its own schedule while the statistics delivery stays complete.
Checka reviewer answers
Reviewed forEach event reaches each of its handlers as a separate delivery with its own retries and dead letter.
Marked guidance: a reviewer answers this question.
Await Target Acceptance#
ruleruleguidancelitespec/ext/outbox#await-target-acceptanceA Worker MUST NOT mark an outbox record complete before the target boundary accepts it.
Applies toartifact: any
Why
Marking a record complete before the target accepts it declares a delivery that never happened, and no retry follows because the record looks done. The Worker waits for the target boundary's acceptance before recording completion.
Checka reviewer answers
Reviewed forCompletion records follow target acceptance, with no early completion.
Marked guidance: a reviewer answers this question.
Bound Delivery Retries#
ruleruleguidancelitespec/ext/outbox#bound-delivery-retriesAn outbox dispatcher MUST use bounded exponential backoff with jitter.
Applies toartifact: any
Why
A failing target with ten thousand pending messages meets a retry storm when every message retries at once on a fixed interval. Bounded exponential backoff with jitter spreads the retries over time and caps the total, so the target recovers instead of drowning.
Checka reviewer answers
Reviewed forRetries run bounded exponential backoff with jitter.
Marked guidance: a reviewer answers this question.
Classify Permanent Delivery Failure#
ruleruleguidancelitespec/ext/outbox#classify-permanent-delivery-failureAn outbox dispatcher MUST classify permanent failures, stop automatic retry at its retry bound, and retain safe investigation and replay data.
Applies toartifact: any
Why
Some failures never clear on retry: a deleted destination, a rejected payload, a revoked credential. Retrying those forever burns resources and buries the real backlog, so the dispatcher classifies them, stops at its retry bound, and keeps the evidence an operator needs to investigate and replay safely.
Checka reviewer answers
Reviewed forPermanent failures stop at the retry bound with safe investigation and replay data retained.
Marked guidance: a reviewer answers this question.
Distinguish Dependency Outage#
ruleruleguidancelitespec/ext/outbox#distinguish-dependency-outageA dispatcher MUST distinguish an unavailable outbox store from a failed claimed message.
Applies toartifact: any
Why
A dead outbox store and a failed delivery ask for different recoveries: the first needs a database restored, the second needs one destination investigated. Each carries its own signal. The store's availability is a gauge the readiness endpoint reads, while a claimed message's outcome is a counter carrying the result, following the OpenTelemetry metrics specification. A single error counter cannot tell an operator whether to restore a database or investigate one destination.
Checka reviewer answers
Reviewed forStore availability reads as a gauge while claimed-message outcomes read as a counter.
Marked guidance: a reviewer answers this question.
Back off Unavailable-Store Polling#
ruleruleguidancelitespec/ext/outbox#back-off-unavailable-store-pollingA Worker MUST apply bounded backoff and rate-limited logging when its outbox store is unavailable.
Applies toartifact: any
Why
A Worker that polls a dead store on a tight loop writes an exception per iteration and fills disks and log quotas before anyone reads the first one. Bounded backoff with rate-limited logging keeps the Worker alive and quiet until the store returns.
Checka reviewer answers
Reviewed forPolling during a store outage backs off with rate-limited logging.
Marked guidance: a reviewer answers this question.
Restrict Message Retry to Claimed Records#
ruleruleguidancelitespec/ext/outbox#restrict-message-retry-to-claimed-recordsA Worker MUST apply message retry and poison handling only to claimed outbox records.
Applies toartifact: any
Why
Retry and poison handling act on a claimed message; when the store itself is down, no claim exists and no message waits for delivery. Running retry logic against the outage double-counts attempts and can dead-letter messages whose only fault was an unreachable store.
Checka reviewer answers
Reviewed forRetry and poison handling touch claimed records only.
Marked guidance: a reviewer answers this question.
Gate Dispatch on Readiness#
ruleruleguidancelitespec/ext/outbox#gate-dispatch-on-readinessA Worker host exposing readiness MUST gate dispatch and stop polling stores that cannot serve requests.
Applies toartifact: any
Why
A host that reports unready still serves traffic it cannot finish when its Workers keep polling dead stores. Gating dispatch on readiness stops the polling, so the host fails fast and recovers instead of churning against storage that cannot answer.
Checka reviewer answers
Reviewed forUnready hosts stop dispatch and dead-store polling.
Marked guidance: a reviewer answers this question.
Version Message Types#
ruleruleguidancelitespec/ext/outbox#version-message-typesAn outbox message MUST use a stable type name and explicit schema version.
Applies toartifact: any
Why
An outbox record outlives the deployment that wrote it, so the dispatcher meets records written by several versions at once. A stable type name with an explicit schema version tells the dispatcher which reader parses each record; without the pair, one shape change breaks every record written before it.
Checka reviewer answers
Reviewed forEach message carries a stable type name with an explicit schema version.
Marked guidance: a reviewer answers this question.
Support Coexisting Message Producers#
ruleruleguidancelitespec/ext/outbox#support-coexisting-message-producersA deployed dispatcher MUST process records from every application version that can coexist during rollout or rollback.
Applies toartifact: any
Why
During rollout and rollback, several application versions share one store and write different valid shapes into it. The dispatcher reads from every version that can coexist, or the deployment itself causes delivery failures.
Each message record therefore carries its own shape version, and the dispatcher selects a reader from that value. Adding an optional member with a defined absent-value behavior needs no new version. Removing a member, renaming one, narrowing a type, or changing a discriminator needs a new version, and the reader for each version stays until no record carries that version.
Checka reviewer answers
Reviewed forThe dispatcher reads records from every coexisting application version.
Marked guidance: a reviewer answers this question.
Publish Backlog Indicators#
ruleruleguidancelitespec/ext/outbox#publish-backlog-indicatorsAn outbox deployment MUST publish pending count, failed count, oldest pending age, attempts, dispatch duration, and success rate.
Applies toartifact: any
Why
Pending count, failed count, oldest pending age, attempts, dispatch duration, and success rate together show whether delivery keeps up: growth in the backlog or age reveals a stall, while duration and success rate reveal a struggling target. Without these indicators, the first sign of failure is a customer asking where their message went.
Checka reviewer answers
Reviewed forThe deployment publishes pending count, failed count, oldest pending age, attempts, dispatch duration, and success rate.
Marked guidance: a reviewer answers this question.
Set Delivery Alerts#
ruleruleguidancelitespec/ext/outbox#set-delivery-alertsAn outbox deployment MUST set alert thresholds as setting records from its use-case delivery target.
Applies toartifact: record:setting
Why
A shipment notice delayed an hour and a receipt delayed a week carry different business consequences, so each use case sets alert thresholds from its own delivery target. Setting records hold those thresholds, so the alert fires when delay passes what the business accepted.
Checka reviewer answers
Reviewed forAlert thresholds sit in setting records derived from each use-case delivery target.
Marked guidance: a reviewer answers this question.
Model Outbox Lifecycle States#
ruleruleguidancelitespec/ext/outbox#model-outbox-lifecycle-statesAn outbox record MUST model its lifecycle as an abstract state base with one sealed record for each of pending, processing, dispatched, and dead-letter.
Applies toartifact: state-record
Why
pending becomes processing, and processing becomes dispatched, pending, or dead-letter, while audited replay moves dead-letter back to pending. Three of the four states carry data the others do not: processing holds a lease expiry, dispatched holds a dispatch time, and dead-letter holds a failure reason and an attempt count. A four-value label forces those onto nullable columns that mean something in one state each, which is the shape litespec/backend/domain#model-every-closed-set-of-domain-values-as-a-closed-type-hierarchy removes everywhere else.
Example
OutboxState.Processing carries one unexpired lease before a dispatcher records OutboxState.Dispatched or a retry state.
Checka reviewer answers
Reviewed forOutbox lifecycle states form sealed records over an abstract base with documented transitions.
Marked guidance: a reviewer answers this question.
Preserve Message Identity During Retry#
ruleruleguidancelitespec/ext/outbox#preserve-message-identity-during-retryAn outbox dispatcher MUST preserve message ID across retry and replay.
Applies toartifact: any
Why
The target suppresses a repeat by recognizing its identity, which works only when every retry and replay carries the same message ID. A dispatcher that mints a new ID per attempt turns each retry into a new message the target processes again.
Checka reviewer answers
Reviewed forMessage ID stays identical across retry and replay.
Marked guidance: a reviewer answers this question.
Audit Outbox Replay#
ruleruleguidancelitespec/ext/outbox#audit-outbox-replayAn outbox replay MUST record actor, reason, time, and failure without editing the payload: an audit record when audit is selected, a standalone record otherwise.
Applies toartifact: any
Why
A replay dispatches a message the system already handled or dead-lettered, so the team records who replayed it, why, when, and which failure it answers. Without that record, a duplicate delivery looks like a dispatcher bug instead of an operator's decision, and nobody can reconstruct the choice.
Checka reviewer answers
Reviewed forEach replay records actor, reason, time, and failure without editing the payload.
Marked guidance: a reviewer answers this question.
Protect Active Worker Compatibility#
ruleruleguidancelitespec/ext/outbox#protect-active-worker-compatibilityNew WebApi code MUST emit a message only after every active Worker can read its type and schema version.
Applies toartifact: endpoint
Why
WebApi writers and Worker dispatchers deploy independently, so a writer can run ahead of the oldest active Worker. When the writer emits a shape that Worker cannot read, those messages dead-letter on arrival through no fault of their own.
Checka reviewer answers
Reviewed forEvery active Worker reads the type and schema version of new messages.
Marked guidance: a reviewer answers this question.
Deploy Readers Before Writers#
ruleruleguidancelitespec/ext/outbox#deploy-readers-before-writersA release MUST deploy compatible readers before writers for additive event versions.
Applies toartifact: any
Why
In a mixed-version rollout, old and new records sit in one store. Deploying compatible readers before the writers that emit new shapes keeps every record dispatchable at every step; the reverse order strands new records until the readers catch up.
Checka reviewer answers
Reviewed forReleases deploy compatible readers before additive writers.
Marked guidance: a reviewer answers this question.
Plan Undispatched Rollback Records#
ruleruleguidancelitespec/ext/outbox#plan-undispatched-rollback-recordsA rollback Runbook MUST include records created by the new writer but not yet dispatched.
Applies toartifact: record:runbook
Why
Rolling back the deployment does not roll back the store: records the new writer staged but nobody dispatched stay behind. The rollback Runbook names how the previous version handles them, or they sit undispatched with no owner and no reader.
Checka reviewer answers
Reviewed forThe rollback Runbook covers records the new writer staged but nobody dispatched.
Marked guidance: a reviewer answers this question.
Record Realtime Need#
ruleruleguidancelitespec/ext/outbox#record-realtime-needA realtime use case MUST document polling behavior, measured cost, update latency, expected connections, and recovery behavior.
Applies toartifact: record:notification
Why
Open connections cost memory, CPU, and operating attention on every host, so the team writes down what polling costs, what the update target requires, and how many connections to expect before choosing transport. The use case page carries that record, so a later reader sees which measurements justified the choice.
Checka reviewer answers
Reviewed forThe use case page holds the polling record, measured cost, update target, expected connections, and recovery behavior.
Marked guidance: a reviewer answers this question.
Reject Cosmetic Realtime Adoption#
ruleruleguidancelitespec/ext/outbox#reject-cosmetic-realtime-adoptionA product MUST NOT activate realtime only to make an interface feel immediate.
Applies toartifact: record:notification
Why
Realtime transport that serves only a feeling of immediacy spends connection capacity and operating attention without moving any target. The extension asks for a measured update requirement first, so polling stays where polling suffices.
Checka reviewer answers
Reviewed forThe realtime decision cites a measured update requirement.
Marked guidance: a reviewer answers this question.
Use Server-Sent Events for Notifications#
ruleruleguidancelitespec/ext/outbox#use-server-sent-events-for-notificationsA one-way server notification use case MUST use server-sent events.
Applies toartifact: record:notification
Why
A notification that flows one way, from server to browser, needs no bidirectional channel with groups and fallback. Server-sent events carry it over plain HTTP, which passes through proxies and firewalls without extra negotiation.
Checka reviewer answers
Reviewed forEach one-way notification travels over the server-sent events boundary in code.
Marked guidance: a reviewer answers this question.
Use SignalR for Interactive Messaging#
ruleruleguidancelitespec/ext/outbox#use-signalr-for-interactive-messagingA use case requiring bidirectional messages, server groups, or transport fallback MUST use SignalR.
Applies toartifact: record:notification
Why
Two-way messages, server-managed groups, and transport fallback need a channel built for interaction, which one-way events cannot provide. SignalR supplies all three, so the use case that needs them takes SignalR instead of stretching events past their shape.
Checka reviewer answers
Reviewed forEach bidirectional use case runs on SignalR groups, messages, or fallback in code.
Marked guidance: a reviewer answers this question.
Record Transport Support#
ruleruleguidancelitespec/ext/outbox#record-transport-supportA realtime use case MUST record its transport choice and expected hosted-platform support.
Applies toartifact: record:notification
Why
Hosted platforms limit connection duration, idle timeouts, and transport upgrades in ways that differ per host. Recording the transport choice with its expected platform support surfaces the mismatch before deployment instead of during the first long-lived connection.
Checka reviewer answers
Reviewed forThe use case page names its transport choice with the expected hosted-platform support.
Marked guidance: a reviewer answers this question.
Declare Realtime Freshness#
ruleruleguidancelitespec/ext/outbox#declare-realtime-freshnessA query served over realtime MUST declare a freshness object with mode of immediate or advisory on its output.
Applies toartifact: record:notification
Why
A realtime channel delivers an update seconds after the change, so the served query promises {"mode": "immediate"}; where the pushed value still awaits a later decision, it promises {"mode": "advisory", "authority": "<record id>"} naming the record that holds the authoritative value. Without the declared freshness, a reader cannot tell whether the live value is final or provisional.
Checka reviewer answers
Reviewed forWhich realtime-served queries lack an immediate or advisory freshness object on their output? Look at the served query beside the realtime use case.
Marked guidance: a reviewer answers this question.
Authenticate and Authorize Subscriptions#
ruleruleguidancelitespec/ext/outbox#authenticate-and-authorize-subscriptionsA realtime server MUST authenticate each connection and authorize every user-, tenant-, or resource-scoped subscription.
Applies toartifact: record:notification
Why
Connecting proves who the caller is; it says nothing about which user's, tenant's, or resource's updates the caller may hear. Each user-, tenant-, or resource-scoped subscription checks its own authorization, so one valid connection cannot listen in on another tenant's data.
Checka reviewer answers
Reviewed forThe connection tests show authenticated connections only, with each scoped subscription authorized.
Marked guidance: a reviewer answers this question.
Revalidate Long-Lived Access#
ruleruleguidancelitespec/ext/outbox#revalidate-long-lived-accessA realtime server MUST revalidate long-lived access according to token and session lifetime.
Applies toartifact: record:notification
Why
A connection can outlive the grant that opened it: tokens expire, roles change, sessions end. Revalidating access on token and session lifetime closes the connection whose grant died instead of streaming updates to a caller who no longer qualifies.
Checka reviewer answers
Reviewed forA connection whose token or session expired loses access in the revalidation tests.
Marked guidance: a reviewer answers this question.
Reconnect and Refresh Authoritative State#
ruleruleguidancelitespec/ext/outbox#reconnect-and-refresh-authoritative-stateA realtime client MUST reconnect with bounded backoff and refresh authoritative state after a connection gap.
Applies toartifact: record:notification
Why
Connections drop, and a notification sent during the gap never arrives. Bounded backoff reconnects without hammering the server, and refreshing authoritative state after the gap replaces whatever the missed notifications would have changed.
Checka reviewer answers
Reviewed forThe client reconnects with bounded backoff and refreshes authoritative state after a gap in the recovery tests.
Marked guidance: a reviewer answers this question.
Tolerate Notification Ordering Differences#
ruleruleguidancelitespec/ext/outbox#tolerate-notification-ordering-differencesA realtime client MUST safely handle duplicate and out-of-order notifications.
Applies toartifact: record:notification
Why
The transport can deliver a notification twice or deliver two of them out of order. A client that applies each notification as an absolute update corrupts its state on the first duplicate; a client built for repeats and reordering converges on the truth either way.
Checka reviewer answers
Reviewed forDuplicate and reordered notifications leave client state correct in the client tests.
Marked guidance: a reviewer answers this question.
Bound Realtime Resources#
ruleruleguidancelitespec/ext/outbox#bound-realtime-resourcesA realtime deployment MUST set four setting-record bounds: concurrent connections per host, receive message size, parallel invocations per connection, and client timeout.
Applies toartifact: record:notification
Why
Each open connection holds memory, each message costs CPU, and a flood of either starves the host. The deployment sets four bounds as setting records - concurrent connections per host, receive message size, parallel invocations per connection, and client timeout - with each value drawn from the deployment's own connection count and message size and the observation that produced it. The platform's realtime security guidance names the same settings.
Checka reviewer answers
Reviewed forThe four deployment bounds live as setting records with their values beside the deployment.
Marked guidance: a reviewer answers this question.
Reject Unbounded Slow-Client Buffering#
ruleruleguidancelitespec/ext/outbox#reject-unbounded-slow-client-bufferingA realtime server MUST NOT buffer an unbounded stream for a slow client.
Applies toartifact: record:notification
Why
A client that reads slower than the server writes grows its buffer without end, consuming host memory meant for every subscriber. Bounded buffering with backpressure disconnects or slows the laggard instead of letting one slow reader starve the rest.
Checka reviewer answers
Reviewed forSlow-client buffering in the server code is bounded with backpressure.
Marked guidance: a reviewer answers this question.
Conventions#
Group Outbox Infrastructure#
conventiondefaultguidancelitespec/ext/outbox#group-outbox-infrastructureKeep outbox record, storage, claim, and dispatch infrastructure under Infrastructure/Messaging/Outbox/.
ReplacementA project names another home for outbox infrastructure in its documentation.
Applies toartifact: any
Why
The outbox record, storage, claim, and dispatch code form one mechanism with one owner. Keeping it under Infrastructure/Messaging/Outbox/ puts every persistent dispatch question in one folder instead of scattered across the project.
Checka reviewer answers
Reviewed forOutbox infrastructure sits under Infrastructure/Messaging/Outbox/, or the project names another home in its documentation.
Marked guidance: a reviewer answers this question.
Isolate the Worker Host#
conventiondefaultguidancelitespec/ext/outbox#isolate-the-worker-hostKeep the outbox Worker host in its own project.
ReplacementA project hosts outbox dispatch inside its request host.
Applies toartifact: any
Why
The Worker runs as its own process with its own lifetime, scaling, and configuration, apart from the request host. Its own project keeps that independence visible and stops request concerns leaking into dispatch configuration.
Checka reviewer answers
Reviewed forThe outbox Worker host lives in its own project, or the project hosts dispatch inside its request host.
Marked guidance: a reviewer answers this question.
Keep Provider Mapping with Integration#
conventiondefaultguidancelitespec/ext/outbox#keep-provider-mapping-with-integrationKeep Integration Event-to-provider mapping in the integration that owns the side effect.
ReplacementA project keeps provider mapping in the shared dispatch path.
Applies toartifact: any
Why
Each provider speaks its own transport with its own mapping, retries, and quirks. The integration that owns the side effect owns that mapping, so a provider change touches one integration instead of the shared dispatch path.
Checka reviewer answers
Reviewed forProvider mapping sits in the owning integration, or the project keeps it in the shared dispatch path.
Marked guidance: a reviewer answers this question.
Dispatch Journey-Step Commands in a New Scope#
conventiondefaultguidancelitespec/ext/outbox#dispatch-journey-step-commands-in-a-new-scopeDispatch a journey-step command through ICommandMediator in a fresh Worker scope.
ReplacementA project runs journey-step commands inline in the dispatcher scope.
Applies toartifact: any
Why
A journey-step command is a full command with validation, authorization, and its own transaction, not a subroutine of the dispatcher. Sending it through ICommandMediator in a fresh Worker scope runs the whole application pipeline for it.
Checka reviewer answers
Reviewed forJourney-step commands run through ICommandMediator in a fresh Worker scope, or the project runs them inline in the dispatcher scope.
Marked guidance: a reviewer answers this question.
Avoid Automatic Durable-Bus Adoption#
conventiondefaultguidancelitespec/ext/outbox#avoid-automatic-durable-bus-adoptionUse project-owned Marten records and Worker dispatch unless a manifest-pinned durable package passes a shared-transaction integration test.
ReplacementA project names a durable bus package as its dispatch transport in its documentation.
Applies toartifact: any
Why
A durable bus package earns its place only when an integration test proves it commits the message in the same transaction as the business work. Until that evidence exists, project-owned Marten records with Worker dispatch carry delivery without depending on behavior nobody verified.
Checka reviewer answers
Reviewed forDispatch runs on project-owned Marten records, or the project names a pinned durable bus package as its transport in its documentation.
Marked guidance: a reviewer answers this question.
Place Realtime Code at Outer Boundaries#
conventiondefaultguidancelitespec/ext/outbox#place-realtime-code-at-outer-boundariesKeep hubs or endpoints in WebApi, publication adapters in Infrastructure, and browser connections under lib/realtime/.
ReplacementA consumer records a realtime-placement convention naming where its transport code lives.
Applies toartifact: record:notification
Why
Hubs, endpoints, publication adapters, and browser connections deal in transport and provider detail, not business rules. Keeping them at the outer boundaries leaves business features owning only the narrow functions they call.
Checka reviewer answers
Reviewed forRealtime transport code sits at the documented boundaries, or the realtime-placement convention names where it lives.
Marked guidance: a reviewer answers this question.
Subscribe Through Narrow Functions#
conventiondefaultguidancelitespec/ext/outbox#subscribe-through-narrow-functionsLet feature modules subscribe through narrow use-case-specific functions.
ReplacementA consumer records a realtime-subscription convention naming how its features subscribe.
Applies toartifact: record:notification
Why
A feature that touches the raw connection spreads subscription strings and payload shapes through business code. Narrow use-case-specific functions show each subscription's inputs and effects beside the feature that owns them.
Checka reviewer answers
Reviewed forFeatures subscribe through use-case-specific functions, or the realtime-subscription convention names how they subscribe.
Marked guidance: a reviewer answers this question.
Dependencies#
Packages: Selecting SignalR adds @microsoft/signalr. The project records a decision for the package and pins it in the manifest.
Related extensions: integrations, jobs.
Guides: None.