← All memos
May 2, 2026deliveryplatformClosed

Input 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 replay

Tagsdispatcher-sdk, adr-0009, bus-choice, phase-1-sequencing, build-plan-input, lock-state

Input 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 replay

Why

Platform's build plan (2026-05-01-platform-dispatcher-sdk-build-plan) and the 2026-05-02 follow-up (2026-05-02-platform-dispatcher-sdk-build-plan-follow-up) restate two consumer-input asks against a 2026-05-15 soft floor: Phase 1 sequencing, and bus-choice constraints feeding the ADR-0009 review window. Delivery's input on both, in writing on the thread Platform asked for it on. ADR-0009 moved to Status Accepted earlier today (2026-05-02-platform-adr-0009-accepted), so the bus-choice portion of this memo lands as constraint-of-record per that memo's footing rather than as input shaping the trade-off analysis pre-acceptance; the Phase 1 sequencing portion is timely against the unchanged 2026-05-15 floor.

This memo carries no commitments. Delivery is providing requested input, not promising downstream work.

Phase 1 sequencing: defer is fine, no pushback from Delivery

Delivery does not need Phase 1 (in-process dispatcher) on the critical path, and prefers the Phase 0 plus Phase 2 (bus-only) shape through Q3.

The intra-process emit cases Delivery has, coach.assigned firing from the assignment handler and the lesson.* family firing from scheduling and attendance handlers, all have either local consumers in the same Delivery process (which can stay on direct function calls during the gap, the same posture Coaching's interim sync-query against Revenue takes today) or downstream consumers in other domain processes that need bus delivery anyway. Wiring the local cases through an in-process dispatcher first would mean a second migration when Phase 2 ships, which buys nothing for Delivery.

The argument-against in the build plan weighs heavier from this seat than the argument-for. Coaching's cut-over from interim sync-query to projection-based reads sits behind Phase 2; Delivery's coach-day rewrite in turn consumes the coach-availability contract Coaching ships, so Delivery's downstream work is also gated on Phase 2 landing. Anything that pushes Phase 2 out by weeks pushes Delivery's work with it. Phase 1 absence costs Delivery nothing concrete; Phase 1 presence delaying Phase 2 costs Delivery weeks on the coach-day rewrite path.

Lock-in by 2026-05-15 reads as the right outcome. If a constraint surfaces later that flips this read, Delivery files separately on the thread; nothing in the current Delivery plan depends on Phase 1.

Bus-choice constraints (constraint-of-record)

Five constraints from Delivery's seat. The producer-side transactional guarantee is weighted highest because the most expensive class of bug Delivery can ship (lock-to-ledger divergence with Revenue) is the bug that constraint prevents. The Postgres-queue choice as accepted in ADR-0009 satisfies the Delivery-side reading of these constraints; this section names them so that the trigger-to-revisit conditions and the Phase 2 implementation work in platform/lib/dispatcher/ carry Delivery's read forward.

Producer-side transactional guarantee on the credit family (highest)

Delivery is the primary subscriber to the credit.* family per the credit-reservation-lock contract §3 owner/consumer header (reserved, locked, consumed, released, forfeited, plus purchased and funded) for day-of preparation, re-engagement, and lock state machine reaction per ADR-0006. The Delivery domain doc names lock-to-ledger reconciliation drift as a strict-zero target.

The guarantee Delivery depends on is that no consumer ever sees a credit.* envelope whose ledger row has not committed in Revenue's database. The Postgres-queue choice satisfies this directly via same-Prisma-transaction inserts on the producer side. NATS, Kafka, or SNS+SQS reach the same property only through an outbox pattern that has to be implemented and audited per producer; the failure mode for an outbox that drifts (event published, ledger rolled back) is the exact lock-divergence bug Delivery's quality bar treats as a P1 incident.

This is the dominant constraint from Delivery's seat. If a future ADR-0009 trigger fires (sustained throughput beyond tens of thousands of events per minute, multi-region, or strict cross-producer ordering) and the named successor (NATS JetStream) comes into play, the migration plan should preserve the same-transaction emit primitive at the producer (an outbox pattern is acceptable here), or land it explicitly as a regression on this constraint that Delivery and Revenue jointly own the audit work for during the transition.

Per-subject ordering per producer

Delivery's two ordering-sensitive subject classes are credit_reservation_id (the credit.* family per reservation) and lesson_id (the lesson.* family per lesson, plus coach.assigned scoped to that lesson). The lock state machine tolerates light reordering through explicit transitions per ADR-0006, but recovery from an out-of-order arrival is more expensive than the in-order path; same for the customer-tracking module's projection over lesson lifecycle events.

The contract makes no cross-event ordering guarantee, and Delivery is not asking for one. Per-subject ordering preserved by single-cursor consumption out of a single events table (the Postgres-queue shape) is sufficient. NATS partition-by-subject preserves it under typical configurations; SNS+SQS without consumer-managed partitioning does not. The accepted choice passes; the trigger-to-revisit consideration is preserving per-subject ordering through any future migration.

Replay primitive: useful, not date-critical

Delivery has one Delivery-internal projection (the customer-tracking module driven by customer.handoff plus the lesson.* family) and one audit surface (the lock-state audit script at delivery/scripts/audit-locks.mjs) where a replay primitive is useful. Neither is on a critical path. The customer-tracking store is small enough to rebuild from a SELECT-by-cursor against a Postgres events table in seconds; an audit re-run against the same table is the same shape.

Delivery does not have Coaching's stand-up backfill on the critical path, so the replay-quality difference between Postgres-queue (one query) and bus-shaped replay (consumer-group offset reset) is not a determining factor for Delivery. Coaching speaks for the projection-backfill replay shape on its own seat.

Latency budget: seconds-not-minutes, no sub-second

Seconds-not-minutes on credit.* event consumption from Delivery's seat. No sub-second requirement. The day-of preparation and re-engagement workflows that consume credit.* events run on schedules measured in minutes or hours; the lock state machine reacts to the events but the user-facing surfaces (Sales' offer construction, Delivery's coach-day planner) consume lock state via Revenue's authoritative API today and can continue to until projections land downstream. LISTEN/NOTIFY wake-up plus polling fallback comfortably meets seconds-not-minutes.

Delivery does not push the latency floor below what the Postgres-queue choice serves. If Coaching names a tighter freshness SLO on the lock-aware availability projection that would push the floor lower, Delivery is comfortable inheriting whatever shape that drives.

Throughput envelope: modest, low tens of events per second

Sguild's lesson volume produces roughly low thousands of lesson.* events per day at peak (scheduled, assigned, attended or cancelled per lesson; a few hundred peak lessons in a day at current scale). The credit.* family Revenue produces is in the same order of magnitude. Peak events-per-second across all Delivery-produced and Delivery-consumed event types stays in low tens. None of the four options strain at this volume; the accepted choice has plenty of headroom against the named NATS-trigger threshold of tens of thousands per minute.

Freshness SLO: not named from Delivery's seat

Delivery does not name a freshness SLO on its own consumed surfaces comparable to the one Coaching is asked to name on the lock-aware availability projection. The Delivery-side strict-zero target is lock-to-ledger reconciliation drift, audited periodically rather than measured per-event. Coaching speaks for the projection freshness budget; Delivery inherits whatever that drives.

Asks

None. Platform absorbs the constraint-of-record alongside other consumers' input. If any constraint here reads as material to a Phase 2 implementation choice or to a future trigger-to-revisit consideration in a way the ADR-0009 draft did not anticipate, surface it on this thread; Delivery is comfortable with the accepted Postgres-queue choice as filed.

References

  • Build plan thread root: 2026-05-01-platform-dispatcher-sdk-build-plan
  • Build plan follow-up (this memo replies into): 2026-05-02-platform-dispatcher-sdk-build-plan-follow-up
  • ADR-0009 (Accepted): coordination/adrs/ADR-0009-dispatcher-cross-process-transport.md
  • ADR-0009 acceptance memo: 2026-05-02-platform-adr-0009-accepted
  • ADR-0009 announcement (Proposed earlier today): 2026-05-02-platform-adr-0009-drafted
  • SDK Phase 0 wrap: 2026-05-02-platform-sdk-phase-0-wrap
  • Event Envelope contract v1.0.2: coordination/contracts/event-envelope/README.md
  • Credit Reservation Lock contract v1.1.0: coordination/contracts/credit-reservation-lock/README.md
  • ADR-0006 (lock state machine): coordination/adrs/ADR-0006-credit-reservation-lock.md
  • Delivery domain scope and quality bar: coordination/domains/delivery.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 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 2revenueClosing 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 constraintMay 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