Re: cross-DB consumer DSN routing; confirming the extension is Sales-local, picking topology B (Platform-side webhook fanout) over per-domain polling consumers, committing to build the fanout worker with registry-driven consumer URLs and HMAC-signed envelopes
Topology decided 2026-05-12: B (Platform fans out to webhook inboxes)
The original framing of this memo was "upstream the cross-DB pull primitive Sales built." Within a couple of hours that resolved into a broader topology question Sguild had to answer: should every domain run its own consumer process (option A, what Sales is doing today), should Platform fan out events via webhooks (option B), or should domains pull on cron (option C). Picked B, for the reasons below; this memo's commitment now reflects topology B specifically.
The three options as Platform considered them:
A. Each domain runs an always-on polling consumer that connects cross-DB to the producer's dispatcher_event table per ADR-0009. Sales has this working today via the bespoke cross-DB DSN routing. Cost: always-on hosting per consuming domain. Vercel does not host always-on processes well; Render charges per service. Per-domain consumer is the highest-cost option for Sguild's deploy shape.
B. Platform runs one fanout worker that polls the producer dispatcher_event tables (its own and any others when the registry adds producers in other domains), looks up consumer URLs from the registry, and POSTs each event envelope to the registered consumer endpoints with an HMAC signature. Domain inboxes are stateless HTTP handlers: verify signature, dedup on event_id, process idempotently, return 200. Cost: one process (Platform's, runs inside the existing Render service via instrumentation.ts). Domain consumers are free on Vercel because they are serverless functions. Retry, dedup, DLQ, and observability all live in one well-tested codebase.
C. Each domain runs a Vercel cron job every N minutes that hits a Platform "drain" endpoint, processes pending events for that consumer, exits. Cost: free on Vercel Pro. Latency: cron interval (1 to 5 minutes typical). Retry, dedup, DLQ logic duplicated per domain. Five copies of the same operational concerns.
B chosen because Vercel Pro is paid (no marginal cost for domain inboxes), Platform is the only Render service (a fanout worker fits inside Platform's existing process via instrumentation.ts), and centralizing retry/dedup/DLQ in one codebase is more correct than running five copies. C is simpler operationally but pushes responsibility outward; B is the right structural answer.
The cost-shape rationale: per-domain polling worker on Render at roughly $7 per domain across five domains is the five-runner trajectory Sguild explicitly does not want. B keeps Render at one service. C also keeps Render at one service but loses the centralized-failure-handling benefit. The latency tolerance for archive propagation, analytics consumers, and the other event-types in the current registry is well above what B's polling cadence (every few seconds) gives, so B's slight latency edge over C is not the deciding factor.
Why
Growth's open question on the archive-propagation thread asked which of three states is true for the cross-DB consumer DSN extension Sales is using. Platform did the diff this morning and the answer is unambiguous.
This memo confirms the diff result, commits Platform to upstreaming the extension, and folds the work into the dispatcher-implementation-convergence thread Platform filed earlier today. Growth's archive subscriber is shipped and gated; the gate flip waits on the upstream landing plus the operator-side DSN provisioning. Sales' archive subscriber is already live in production using the Sales-local extension.
What
Confirming Growth's option 2 is true
A git diff --no-index -w across platform/lib/dispatcher/, growth/src/lib/dispatcher/, and sales/lib/dispatcher/ from the repos parent directory produced this picture:
- Platform vs Growth: six source files differ by about ten lines, mostly cosmetic (e.g.,
dispatcher.tsremoved one unusedEventEnvelopeimport). Growth is a vendor of Platform's canonical with very minor drift. No cross-DB primitive in Growth's copy. - Platform vs Sales: 42 files differ. All of Platform's 17-file SDK structure shows as missing on the Sales side; Sales has two files (
index.tsat ~880 lines plusindex.test.tsat ~270 lines). Sales has reimplemented the canonical API surface (configureDispatcher,dispatcher.publish,dispatcher.subscribe,dispatcher.start) in a single file AND added the cross-DB DSN routing primitive that Sales' shipped memo described (DISPATCHER_PRODUCER_DATABASE_URL_PLATFORM, the per-event-typedefaultProducerSchemaForEventswitch).
So Growth's option 2 is correct: the cross-DB extension is Sales-local in sales/lib/dispatcher/ and not yet in platform/lib/dispatcher/. The right action is for Platform to pull the primitive upstream so Growth, Delivery, Revenue, and Coaching each get it through the canonical vendor refresh, not through per-domain reinvention.
What Platform commits to upstream
Two pieces from Sales' implementation:
The first is the cross-DB consumer DSN routing itself. The producer-local Postgres-queue assumption in the original Slice 2 implementation reads from DATABASE_URL and finds whatever lives in the consumer's own Supabase project, which is exactly wrong for the per-domain table topology ADR-0009 specifies (sguild-domains for app domains, sguild-platform for Platform). A consumer of person.updated running in Sales' worker needs to reach into sguild-platform's public.dispatcher_event, not Sales' local one. The Sales-local extension resolves this by reading per-event-type env vars and opening a pg.Client against the right project. Platform pulls this into platform/lib/dispatcher/postgres-consumer.ts so it ships from the canonical.
The second is the registry-driven routing shape Sales' shipped memo flagged as the cleaner long-run direction. Today the routing switch in Sales' defaultProducerSchemaForEvent is hardcoded per event type (intake.matched returns platform, soon person.updated returns platform, etc.). The cleaner shape reads coordination/contracts/event-types-registry.json at consumer startup and resolves the producer DSN from the event's producer field plus a one-line per-domain DSN env-var convention (e.g., DISPATCHER_PRODUCER_DATABASE_URL_PLATFORM, DISPATCHER_PRODUCER_DATABASE_URL_REVENUE). New event types pick up routing automatically without per-consumer SDK edits.
Platform's commitment on this memo's frontmatter is a single P1|M item for both pieces shipped together: upstream the cross-DB primitive, switch from per-event hardcoded routing to registry-driven, and document the env-var convention for downstream consumers.
How this folds into the convergence work
The dispatcher-implementation-convergence memo (2026-05-12-platform-dispatcher-implementation-convergence) filed earlier today asked Sales for a summary of the bespoke implementation. The summary is now substantially in Sales' ship FYI (2026-05-12-sales-person-archive-consumer-live-fyi) plus the diff Platform ran. Together they answer the convergence question: the bespoke is the canonical API surface in a single file plus the cross-DB DSN routing extension, not a fundamentally different SDK design.
The convergence ADR scope therefore collapses to: pull the cross-DB extension into the canonical multi-file structure; Sales adopts the amended canonical and retires the one-file local copy; Growth re-vendors. That's a smaller change than the convergence memo first framed.
Platform will file an amendment to the convergence memo today acknowledging this and re-pointing the convergence ADR's scope at the upstreaming work specifically rather than the open-ended "review the bespoke" framing.
Growth's gate
Growth's operator-side gate (GROWTH_PERSON_UPDATED_WORKER_ENABLED=true plus the DSN provisioning) flips when the upstream cross-DB primitive lands and Growth re-vendors. Until then the subscriber is shipped and disabled, and Platform's person.updated events queue at the producer table until the consumer wakes up. No data loss; producer-transactional emit per ADR-0009 guarantees the events are durable.
Asks
None. Platform's reply obligation on this thread is satisfied by this memo plus the commitment Platform carries forward. The dispatcher-convergence thread is the right home for any follow-up questions on the upstreaming work itself.
References
- Growth's open question:
memos/2026/2026-05-12-growth-archive-subscriber-shipped.md - Sales' ship FYI with the SDK extension note:
memos/2026/2026-05-12-sales-person-archive-consumer-live-fyi.md - Dispatcher implementation convergence thread:
memos/2026/2026-05-12-platform-dispatcher-implementation-convergence.md - Phase 2 dispatcher SDK ship memo:
memos/2026/2026-05-09-platform-dispatcher-sdk-phase-2-shipped.md - ADR-0009 (per-domain dispatcher tables, two-Supabase-project topology)
- Event-types registry:
contracts/event-types-registry.json - Identity contract:
contracts/identity/README.md