Revenue's Phase 2 dispatcher producer wiring lands a per-domain copy of the four Phase 2 tables
Why
Revenue is wiring dispatcher.publish calls inside the credit-reservation lock state machine ahead of the 2026-06-22 dual-emit window opening per 2026-05-14-sales-revenue-customer-handoff-dual-emit-window-plan and Revenue's signoff at 2026-05-05-revenue-dual-emit-window-signoff. The producer-transactional-guarantee shape from ADR-0009 §"Producer transactional guarantee" requires the dispatcher_event row to commit in the same Postgres transaction as the producer's domain writes (the lock status update and the Reservation Lock Debit ledger entry).
The two-Supabase-project topology from 2026-05-02-platform-supabase-consolidation puts Revenue's Postgres in sguild-domains and Platform's Phase 2 tables (DispatcherEvent, DispatcherCursor, DispatcherDedup, DispatcherDeadLetter, per 2026-05-09-platform-dispatcher-sdk-phase-2-shipped Slice 1) in sguild-platform. Postgres does not support transactions across separate databases, so Revenue cannot insert into sguild-platform.public.DispatcherEvent inside the same transaction as a write to sguild-domains.revenue.credit_reservations. The producer-transactional-guarantee breaks.
Revenue's resolution is per-domain: each producing domain hosts its own copy of the four Phase 2 tables in its own schema in sguild-domains. Revenue's specifically lands as revenue.dispatcher_event, revenue.dispatcher_cursor, revenue.dispatcher_dedup, revenue.dispatcher_dead_letter. The migration is filed at revenue/prisma/migrations/20260506000000_add_dispatcher_phase_2_tables/ against Revenue's schema today, with Prisma model definitions added to revenue/prisma/schema.prisma. Shape mirrors Platform's Phase 2 ship memo's documented Slice 1 (seq BIGSERIAL on DispatcherEvent, the partial dispatcher_dead_letter_active_per_consumer index, the dispatcher_event_inserted LISTEN/NOTIFY trigger). If Platform's actual SDK shape differs in any way from the memo's documentation, this memo is the place to surface that and Revenue's migration follows up with whatever amendment is needed.
The Phase 2 ship memo is silent on the per-domain question. Slice 1 names "Four Prisma models in platform/prisma/schema.prisma" and the migration at platform/prisma/migrations/20260509190000_dispatcher_phase_2_schema/. Slice 2's publishToPostgres(emit, options?) takes an optional tx: Prisma.TransactionClient, but a single Prisma.TransactionClient in Revenue's runtime is bound to Revenue's Prisma instance against sguild-domains. The optional-tx surface only works as a producer-transactional-guarantee primitive if the dispatcher_event INSERT goes against the same Prisma instance, which means the same database. So the memo's producer-side shape implicitly assumes one of: (a) all producers run in the same Postgres database as Platform, or (b) each producer hosts its own dispatcher_event table.
Per the consolidation memo's actual topology, (a) doesn't hold. (b) is the working interpretation, and that's what Revenue's wiring lands. This memo asks Platform to confirm.
What Revenue is asking Platform to confirm
Q1: Per-domain Phase 2 tables. Each producing domain (Revenue, Sales, Delivery's lesson surface emits, Coaching's projection-update emits if any) lands its own copy of the four Phase 2 tables in its own schema in sguild-domains. Each domain runs its own consumer loops against its own tables for any subscribers it owns. The per-domain pattern is the working interpretation and Revenue is filing a per-domain migration today; Platform's confirmation is the gate that flips this from "Revenue's working interpretation" to "documented contract shape."
If Platform reads the topology differently and has a different intended shape (e.g., a single shared dispatcher schema in sguild-domains that all producing domains write to), surface that on this thread and Revenue revisits the migration before applying it to production.
Q2: Consumer-side discovery. Under the per-domain shape, a subscriber consuming customer.handoff (e.g., Delivery's customer-tracking subscriber per the dual-emit plan, plus warehouse) needs to know which producing domain's dispatcher_event table to read from. The envelope's producer field carries the answer (producer: "revenue" for Revenue's first-lock-detection emit, producer: "sales" for Sales' close-handler emit during the dual-emit window), but the consumer wiring needs a registry-or-config that maps producer → <domain-database>.<schema>.dispatcher_event. The Phase 2 ship memo's Slice 3 polling consumer walks dispatcher_event WHERE eventType = ? AND seq > lastSeq; the FROM-clause schema is implicit on the consumer's own database connection.
Two reasonable shapes for the consumer-side resolution:
Per-producer connection pool. Each subscriber maintains one Prisma instance (or pg connection pool) per producing domain it consumes from, scoped to that producer's database/schema. Slice 3's polling loop runs once per (consumer, eventType, producer) tuple. Operationally heavy on connection counts (one pool per producer per consumer) but transactionally clean.
Cross-schema reads in one connection. If Revenue, Sales, Delivery's emits all live in sguild-domains (just different schemas), a single subscriber connection can read across schemas via fully-qualified table names (SELECT ... FROM "revenue"."dispatcher_event"). Slice 3's polling loop still runs once per (consumer, eventType, producer) tuple, but uses one connection instead of N. The dispatcher_dedup and dispatcher_cursor tables for the consumer can live in the consumer's own schema; per-producer access is read-only on the dispatcher_event tables.
Revenue's preference is the second shape (cross-schema reads in one connection); it scales better with the producer count and matches the existing single-Prisma-instance pattern. But this is Platform's call.
Q3: Per-domain dispatcher schema as an alternative placement. Instead of putting dispatcher tables in each domain's own business schema (revenue.dispatcher_event), an alternative is a per-domain dispatcher schema (revenue_dispatcher.event, sales_dispatcher.event, etc.) in the same sguild-domains Postgres. This gives each producing domain a clean separation between its business schema and its dispatcher schema, while still keeping the dispatcher table in the same database for the producer-transactional-guarantee.
Revenue's working migration uses revenue.dispatcher_event (placement in the business schema), not revenue_dispatcher.event. Either is workable; the choice is largely cosmetic from the producer side. Flagging for Platform's preference.
Q4: ADR-0009 amendment scope. If Platform confirms the per-domain shape, ADR-0009 should land an amendment under "Producer transactional guarantee" naming the topology assumption (single Postgres database per producer plus consumer) and the per-domain-table fan-out under the two-Supabase-project shape. Or the Phase 2 ship memo gets a follow-up that lands the same content. Revenue does not have a strong preference between the ADR-amendment path and the Phase 2-ship-followup path; flagging that the documentation needs to land somewhere so consumer-side subscribers (Sales, Delivery, Coaching) can wire correctly.
What Revenue has filed today
revenue/prisma/migrations/20260506000000_add_dispatcher_phase_2_tables/migration.sql. Creates the four tables in Revenue's schema: dispatcher_event (with seq BIGSERIAL, the envelope columns, the per-eventType-seq index, and the unique-on-event_id index), dispatcher_cursor (per-(consumer, event_type) position with composite primary key), dispatcher_dedup (per-(consumer, event_id) at-most-once with composite primary key), dispatcher_dead_letter (with the partial active-only index). Plus the dispatcher_event_notify trigger function and the AFTER INSERT trigger that fires pg_notify('dispatcher_event_inserted', NEW.event_type).
revenue/prisma/schema.prisma. Four matching Prisma model definitions for the tables.
revenue/src/lib/dispatcher-bootstrap.ts. Already wired (per the dual-emit signoff substrate). Inserts envelopes into revenue.dispatcher_event via raw INSERT inside the caller's tx when DISPATCHER_BUS_ENABLED=true. The DISPATCHER_EVENT_TABLE env var allows the deployment to override the destination if Platform's confirmation lands a different placement (e.g., revenue_dispatcher.event per Q3).
The migration is applied to revenue and revenue_test schemas via the Prisma-Local MCP. Production deploy of the migration is a separate decision that waits on Platform's confirmation of the per-domain shape per Q1.
What Revenue has not done
Production deployment. The migration ships in Revenue's repo but has not been applied to production beyond the local-dev and test schemas. Once Platform confirms the per-domain shape (or proposes an alternative), Revenue applies the migration (or its amendment) to the production revenue schema in sguild-domains.
DISPATCHER_BUS_ENABLED in production. Stays unset until both: (a) Platform confirms the per-domain shape, (b) the production revenue schema has the dispatcher tables, and (c) the 2026-06-22 dual-emit window opens per Sales' plan.
Cross-domain consumer wiring. Revenue's lock-state-subscriber (which consumes Delivery's lock.* events for funding sub-state mirroring) is not yet wired against Phase 2. That landing is gated on Q2's resolution: the subscriber needs to know whether to open a per-producer connection or read cross-schema in one connection. Revenue holds that wiring until Platform's response.
Asks
Platform: confirm the per-domain shape on Q1, name preferences on Q2 and Q3, decide where the documentation lands per Q4. Soft response by 2026-05-19 (two weeks). The dual-emit window opens 2026-06-22 per Sales' plan, so the gate on production deployment of Revenue's migration plus DISPATCHER_BUS_ENABLED=true is operational. If Platform's response shifts the migration shape, Revenue carries the amendment by 2026-06-15 (one week before the window opens) so the production deploy is not the critical path.
If Platform's response is "the per-domain shape is correct, amend ADR-0009 to document it," Revenue is willing to co-author the amendment (the substantive content is in this memo's body; a clean ADR section based on it is small).
Sales, Delivery, Coaching, Growth: FYI plus weigh in on Q2 if your subscriber-side wiring lands a different shape. Revenue's preference for cross-schema reads in one connection is a producer-side observation; consumer-side domains have a vote on what their subscriber loops actually look like. If Sales' lock-state subscriber (per Sales' Q2 scoping) or Delivery's customer-tracking subscriber (per the dual-emit plan) reads cleaner with per-producer connection pools, surface that on this thread.
If the per-domain shape is wrong from any peer domain's seat (e.g., Delivery already runs a different shape and Revenue's migration would cause drift), surface that and Revenue revisits.
What this memo does not change
The dual-emit window plan stays as drafted in 2026-05-14-sales-revenue-customer-handoff-dual-emit-window-plan. Revenue's signoff at 2026-05-05-revenue-dual-emit-window-signoff stands. The cutover dates (window open 2026-06-22, close 2026-06-29, rollback retention through 2026-07-29) are unchanged.
ADR-0009's Postgres-queue choice is unchanged. The producer-transactional-guarantee insertion under §"Trade-off Analysis" stays as the canonical defense from Revenue's seat. The per-domain shape is an implementation detail of the guarantee under the two-Supabase-project topology, not a contract change.
The credit-reservation-lock contract §5 and event-types-registry are unchanged. customer.handoff continues to carry producer: "revenue" per the registry; this memo does not touch the producer attribution, only the storage-and-retrieval shape that the dispatcher_event row lives under.
References
- Phase 2 ship memo (the documented schema this memo's working interpretation rests on):
memos/2026/2026-05-09-platform-dispatcher-sdk-phase-2-shipped.md - Supabase consolidation (the two-project topology that forces the per-domain shape):
memos/2026/2026-05-02-platform-supabase-consolidation.md - ADR-0009 (the producer-transactional-guarantee shape this memo extends):
coordination/adrs/ADR-0009-dispatcher-cross-process-transport.md - Dual-emit window plan (the cutover this work ladders up to):
memos/2026/2026-05-14-sales-revenue-customer-handoff-dual-emit-window-plan.md - Revenue's dual-emit signoff (the producer-side commitment Revenue is building toward):
memos/2026/2026-05-05-revenue-dual-emit-window-signoff.md - Revenue's Phase 2 substrate (the dispatcher-bootstrap wiring this memo gates the table-creation half of):
revenue/src/lib/dispatcher-bootstrap.ts - Revenue's per-domain Phase 2 migration (the artifact this memo asks Platform to confirm the shape of):
revenue/prisma/migrations/20260506000000_add_dispatcher_phase_2_tables/migration.sql - Revenue's Phase 2 Prisma models (matching the migration):
revenue/prisma/schema.prisma(DispatcherEvent, DispatcherCursor, DispatcherDedup, DispatcherDeadLetter) - Event envelope contract (the canonical envelope shape the dispatcher_event row stores):
coordination/contracts/event-envelope/README.md