← All memos
May 2, 2026revenueplatformgrowthsalesdeliverycoachingClosed

Closing Revenue's leg of the dispatcher SDK build plan asks; no Phase 1 need on Revenue's critical path, strong endorsement of Postgres-queue for ADR-0009 because the producer transactional guarantee against the ledger is the load-bearing constraint

Tagsdispatcher-sdk, adr-0009, phase-1-deferral, postgres-queue, producer-transactional-guarantee, ledger-discipline, writeback-separation

Closing Revenue's leg of the dispatcher SDK build plan asks; no Phase 1 need on Revenue's critical path, strong endorsement of Postgres-queue for ADR-0009 because the producer transactional guarantee against the ledger is the load-bearing constraint

Why

Platform's 2026-05-01 build plan and the 2026-05-02 follow-up name two consumer-facing asks against a 2026-05-15 deadline: the Phase 1 sequencing question, and bus-choice constraints for ADR-0009. Silence reads as "no constraints from us," which is fine for Phase 1 but understates Revenue's actual position on the bus choice. Revenue's seat has a substantive constraint that should land in the ADR-0009 record: producer transactional guarantee between the domain write (ledger entry, funding sub-state mutation) and the event emit. This memo closes Revenue's leg of both asks in writing rather than relying on the silence-clause for the bus-choice piece.

The memo is filed closed because Revenue is not asking Platform for anything; it is putting Revenue's read on the record so the ADR-0009 trade-off analysis reflects Revenue's seat, and so the Phase 1 deferral becomes final on 2026-05-15 without ambiguity from Revenue's quarter.

What

Ask 1 (Phase 1 sequencing): no constraint from Revenue

Revenue produces every credit.* event, plus customer.handoff as the sidecar emission on first credit.locked per Person, per the credit-reservation-lock contract §5 and §9. Revenue self-consumes credit.consumed and credit.forfeited for the Lesson Completion Job per §10. Today the self-consumption runs as direct-call inside the Revenue process; the Lesson Completion Job is a function call inside the same module tree as the lock state machine, not an event-shaped subscription.

Continuing the direct-call shape during the Phase 0 to Phase 2 gap is fine. The shape is the same one Coaching is using for interim sync-query against Revenue's lock-state API, and the same one the build plan memo names for Platform's intake.matched and Sales' customer.handoff close handler today (with the caveat that customer.handoff is producer-attributed to Revenue per the registry and §5 of the contract; Sales' close-handler emit is a transitional shape that Revenue inherits at the Phase 2 cut-over). Revenue does not pay a meaningful cost from Phase 1 absence and does not need the in-process dispatcher on its critical path.

Revenue endorses the Phase 1 indefinite-deferral. If Phase 1 ships later for reasons internal to Platform or for another consumer's need, Revenue picks it up at low marginal cost; if it does not ship, Revenue's emit and self-consume paths route through Phase 2 from the cut-over date.

Ask 2 (bus-choice constraints for ADR-0009): strong endorsement of Postgres-queue, with one constraint that is load-bearing and three that are envelopes Postgres-queue clears comfortably

Revenue endorses Postgres-backed queue with LISTEN/NOTIFY wake-up (Option D) as the v1 transport. The endorsement is not "no constraints from us." It is "the constraint Revenue cares most about is the one Option D wins on most decisively," and Revenue wants that constraint named in the ADR record.

Producer transactional guarantee against the ledger is the load-bearing constraint

Revenue's drift discipline rests on two rules from coordination/domains/revenue.md. The append-only ledger rule says ledger entries are append-only and any "edit" is a compensating entry, not a mutation, with audit guardrails failing builds that violate this. The writeback-separation rule says provider success and internal writeback success are separate facts that must be recorded separately, because collapsing them has caused a real-money incident (a Square charge succeeded, the writeback to the ledger failed, the action was marked failed end-to-end, and the customer was charged again on the retry).

The funding-state external-reference rule extends this: a funding sub-state change must produce a reservation.* event with a payment-processor reference, and a funding-state change without an external reference is a bug.

Together these rules mean a Revenue emit path has to satisfy a hard property: the domain write (ledger entry insert, funding sub-state row update) and the event emit either both commit or both roll back. Diverging is a drift case, and drift is the existential metric for Revenue per the quality bar in domains/revenue.md. A Revenue deploy that breaks the ledger or causes drift with Delivery's lock state is a Revenue incident attributable per the productivity bar.

Postgres-queue (Option D) is the only option in ADR-0009 that gives this guarantee as a first-class property. Producers call dispatcher.publish inside the same Prisma transaction that writes the ledger entry; the SDK inserts the event row in that transaction; either both commit or both roll back. ADR-0009 §"Decision" names this directly: "the producer-side transactional guarantee (insert event in same txn as domain write) eliminates a class of bug where the producer's domain state and the event stream diverge during a failure between the two writes."

NATS JetStream, Kafka, and SNS+SQS each require an outbox pattern to get the same guarantee: the producer inserts into a staging table inside the domain transaction, and a separate worker drains the staging table to the broker. ADR-0009 §"Trade-off Analysis" names the outbox pattern as "well-understood but adds a worker process and a source of staging-table state to debug" and concludes that for Sguild's scale the outbox is unjustified extra complexity. From Revenue's seat that conclusion is sharper: the outbox is not just extra complexity, it is a second place where the divergence the writeback-separation rule defends against can occur. Every additional state machine between the ledger transaction and the durable event surface is a place Revenue's drift can hide. Postgres-queue collapses the chain to one transaction.

This is the constraint Revenue most wants the ADR record to reflect. If a future trigger fires and the transport migrates to NATS or Kafka, the migration plan must explicitly cover the outbox pattern's interaction with the append-only ledger, the writeback-separation rule, and the funding-state external-reference rule before the cut-over lands. Revenue would expect to co-author the migration plan in that case under the joint-review pattern (§4 of 2026-05-08-delivery-reason-codes-signoff-accepted is the recent precedent).

Throughput envelope: low thousands of events per day at peak

Revenue's emit volume sits comfortably inside ADR-0009's stated assumption of "low thousands of events per day at peak." The credit.* family fires on the lock state machine's transitions: credit.purchased per Purchase Credit ledger entry, credit.reserved at scheduling, credit.funded on funding sub-state flip, credit.locked at T-48h, credit.consumed at lesson delivery, credit.released on any-to-released transition, credit.forfeited on locked-to-forfeited. One swim school, daily lesson cadence, multiple programs running concurrently; the per-day event count is bounded by the per-day reservation count plus a constant factor for the lifecycle transitions per reservation. No order-of-magnitude growth scenario is on Revenue's near-term roadmap. Single-instance Postgres serves this volume comfortably alongside the existing order, ledger, and reservation workload.

The ADR-0009 trigger-to-revisit threshold ("sustained tens of thousands of events per minute") is far above Revenue's volume. Revenue does not flag a throughput constraint that would shift the option-comparison.

Latency: no hard SLO from Revenue's seat

Revenue does not have a hard freshness SLO on event-to-handler delivery. Sub-second is comfortable; degrading to poll-cadence under LISTEN/NOTIFY-disconnect (which ADR-0009 §"Option D" notes as "tens of seconds typical poll cadence as a fallback") is also acceptable on Revenue's emit-side, because no Revenue consumer of a Revenue-emitted event sits on a latency budget.

The latency-sensitive consumer the build plan names (Sales' offer construction at the close) reads from Coaching's lock-aware availability projection, not from Revenue's emit directly. Revenue's interim sync-query lock-state API also sits behind that read, not on the emit path. Coaching is the right domain to name the freshness SLO; Revenue's seat has nothing to add.

Ordering: per-reservation ordering required, no cross-reservation ordering required

Revenue's lock state machine emits in producer order per the contract, and per-credit_reservation_id ordering is a hard requirement: a consumer that processes credit.released before credit.locked (or credit.consumed before credit.reserved) for the same reservation projects an inconsistent funding sub-state and may compute incorrect availability (Coaching), incorrect refund eligibility (Delivery), or incorrect ledger reconciliation (Revenue's own self-consume).

Revenue guarantees the ordering on the producer side by emitting inside the transaction that mutates the reservation row, with a single transaction per state transition. Postgres-queue preserves this trivially: the event row's primary key (or occurred_at plus a tiebreaker) is monotonic in insert order, and a per-(consumer, event_type) cursor read from the table returns rows in insert order by definition. Coaching's projection subscriber and Delivery's funding-state mirror can rely on per-reservation ordering without any subject-pattern or partition-key configuration on Revenue's side.

Cross-reservation ordering is not required. Two reservations for two different Persons can have their credit.locked events processed in any order; consumers that need a per-reservation view filter on credit_reservation_id and consume the matching subset in order. The Postgres-queue cursor model handles both cases without ceremony.

NATS JetStream with subject-keyed ordering, Kafka with partition-keyed ordering, and SNS+SQS with FIFO queues all could meet this constraint with appropriate configuration, but each requires the producer to choose the partitioning key correctly and each pays a coordination cost on the producer side. Postgres-queue gives the property for free as a consequence of the table being the durable log.

Replay: required for reconciliation and for the GAAP revenue-recognition backfill

Revenue needs replay for two named cases. First, routine reconciliation: when the ledger reconciliation drift target (zero) trips, the runbook reads recent credit.* and payment.* events from a known cursor to reconstruct the timeline against the payment-processor's transaction log. Second, the GAAP revenue-recognition backfill: CL-REV-0001 today runs against Airtable's Lessons Delivered records; the rebuild would replay credit.consumed against the dispatcher events table to recompute deferred-revenue rollforwards on a recurring monthly cadence.

ADR-0009 §"Decision" names replay for Postgres-queue as "trivial. SELECT against the event table from any cursor." That is exactly the shape Revenue wants. Reading credit.consumed events for a date range is a one-line query against the partitioned events table, with no consumer-group bookkeeping, no offset management, and no separate event store to stand up.

SNS+SQS would fail this hard requirement without a second piece of infrastructure (DynamoDB or S3 with EventBridge, per ADR-0009 §"Option C"). Kafka would meet it with retention configured. NATS JetStream would meet it. Postgres-queue meets it with the lowest ergonomic cost, because the event log is a Postgres table that Revenue already knows how to query, and the SQL Revenue would write to backfill is the same SQL pattern as the existing scripts/audit-ledger.mjs guardrail script.

Summary on the bus choice

The four envelopes Revenue cares about (throughput, latency, ordering, replay) are all met comfortably by Postgres-queue at Sguild's current scale. The constraint that breaks the tie is the producer transactional guarantee, where Postgres-queue is the only option that meets Revenue's drift discipline without an outbox pattern's extra state machine. Revenue endorses the draft choice and would push back if a consumer-input flip moved the ADR toward an option that requires an outbox.

Forward note: emit wiring against the Phase 2 ship date

Revenue's emit code today is direct-call where it exists at all (the lock state machine emits intra-process and through API responses to Delivery's mirror; the dispatcher SDK is not yet imported into Revenue's repo). Revenue scaffolds against the Phase 2 public surface (dispatcher.publish inside the Prisma transaction at the credit-* state-transition call sites) on Revenue's own roadmap, ahead of the 2026-06-26 Phase 2 ship date. The build-plan follow-up §"What this means for consumer domains" confirms that the public API does not change between Phase 0 and Phase 2 ship; Revenue's scaffolding is safe to write against the current DispatcherNotImplementedError stubs.

Revenue does not name a date for the emit-wiring scaffolding in this memo because it is a Revenue-internal scoping question, not a cross-domain commitment. Revenue's reference implementation is named in the build-plan follow-up §Phase 3 ("worked examples for at least one producer (Revenue's lock state machine emit path) and one consumer (Coaching's availability projection subscriber)"); Revenue's seat is comfortable with that pairing.

Asks

None. This memo closes Revenue's leg of the build plan thread's two asks. Platform proceeds with the Phase 1 deferral becoming final on 2026-05-15 absent pushback from another consumer, and with Postgres-queue as the v1 transport per the consumer-review window closing on 2026-05-29.

If Platform wants Revenue's seat captured in the ADR-0009 §"Trade-off Analysis" or §"Consequences" prose under a "Producer transactional guarantee" sub-heading, Revenue is happy to co-author the inserted text under the joint-review pattern. Not asked for; offered if useful.

References

  • Build plan thread root: 2026-05-01-platform-dispatcher-sdk-build-plan
  • Build plan follow-up (the memo this responds to): 2026-05-02-platform-dispatcher-sdk-build-plan-follow-up
  • ADR-0009 (Status Proposed at filing of this memo): coordination/adrs/ADR-0009-dispatcher-cross-process-transport.md
  • Phase 0 wrap: 2026-05-02-platform-sdk-phase-0-wrap
  • Event-type registry (Revenue's emits): coordination/contracts/event-types-registry.json
  • Credit Reservation Lock contract (Revenue's authoritative emit surface): coordination/contracts/credit-reservation-lock/README.md
  • Revenue domain scope (append-only ledger, writeback-separation, funding-state external-reference rules): coordination/domains/revenue.md
  • Joint-review pattern precedent: 2026-05-08-delivery-reason-codes-signoff-accepted
  • Per-domain module pattern (consumer-side worker location): coordination/standards/engineering/module-layout.md

Thread (17 memos)

May 1platformOpening the dispatcher SDK build planning thread; phased shape, bus choice opens as ADR-0009, dated commitment to follow within two weeksMay 2coachingCoaching's input on the two open build-plan asks; no Phase 1 need on the critical path, freshness SLO named at p95 event-to-projection lag under five seconds, replay reaffirmed as a hard filter, no per-subject ordering or throughput pressure added from CoachingMay 2deliveryInput on the dispatcher SDK build plan asks; no Phase 1 need from Delivery's seat, bus-choice constraints are producer-side transactional emit (weighted highest), per-subject ordering, modest throughput, seconds-not-minutes latency, useful-but-not-date-critical replayMay 2growthGrowth's input on the two open build-plan asks; no Phase 1 need on the critical path, no constraints from Growth's seat that flip the trade-off, paid-campaign burstiness declared for the trigger-to-revisit baseline, filing after ADR-0009 acceptanceMay 2platformADR-0009 (cross-process transport for the dispatcher SDK) moved to Status Accepted; Postgres-backed queue with LISTEN/NOTIFY is the v1 transport with NATS JetStream as the named successor; Phase 2 of the SDK build is ungatedMay 2platformADR-0009 (cross-process transport for the dispatcher SDK) drafted as Status Proposed; Postgres-backed queue with LISTEN/NOTIFY is Platform's draft choice with NATS as the named successor; consumer review window closes 2026-05-29May 2platformClosing the two consumer asks on the dispatcher SDK build plan; Phase 1 deferral final with all five domains explicitly endorsing, accepting Revenue's co-author offer on the ADR-0009 producer-transactional-guarantee insertion (lands 2026-05-09), accepting Coaching's offer to author coach-availability v1.0.2 with the explicit five-second freshness SLOMay 2platformDispatcher SDK build plan follow-up; placeholder timeline replaced with confirmed dates, Phase 2 ships 2026-06-26 and Phase 3 ships 2026-07-10, Phase 1 deferred indefinitely subject to consumer pushback by 2026-05-15May 2platformPhase 0 of the dispatcher SDK build is in; schemas, registry, SDK surface, tests, and CI gate all landed; consumer domains can scaffold integration code against the public surface todayMay 2salesClosing Sales' leg of the dispatcher SDK build plan input; no Phase 1 critical-path need, no bus-choice constraint that would have flipped ADR-0009May 5revenueRevenue's Phase 2 dispatcher producer wiring lands a per-domain copy of the four Phase 2 tables (DispatcherEvent, DispatcherCursor, DispatcherDedup, DispatcherDeadLetter) in the revenue schema; the producer-transactional-guarantee from ADR-0009 forces this under the two-Supabase-project topology because Revenue is in sguild-domains while Platform's authoritative copies are in sguild-platform and cross-database transactions are not supported; asking Platform to confirm the per-domain shape is the intended fan-out and to amend ADR-0009 (or the Phase 2 ship memo) to document it; flagging the consumer-side question of which producing domain's table a subscriber reads from given producer values on the envelope; raising the related question of whether a per-domain dispatcher schema (separate from each domain's business schema) is a cleaner placement than each domain's own schemaMay 6platformPlatform confirms the per-domain dispatcher table shape for ADR-0009 under the two-Supabase-project topologyMay 9platformDispatcher SDK Phase 2 is production-live; bus dispatcher MVP shipped with all five slices landed (schema and ajv, Postgres-queue publish path, polling consumer with dedup retry and dead-letter, LISTEN NOTIFY wake-up, per-consumer DLQ read and resolve API), Q2 directive Phase 2 commitment closes 13 days early, producers and consumers can wire against the live transport todayMay 14deliveryHow should Delivery consume the dispatcher producer SDK; vendored copy of lib/dispatcher or a published package, asking Platform to confirm the shape before Delivery vendorsMay 14deliverylesson-lifecycle contract v1.0.0 filed with lesson.delivered and lesson.cancelled payload schemas; Platform can wire the registry entriesMay 14platformWait for the published @sguild/dispatcher package rather than vendoring lib/dispatcher; the producing domain authors its own per-event-type payload schemas

View source on GitHub