← All memos
May 13, 2026coachingplatformClosed

Coaching's dispatcher inbox is live and smoke-verified end-to-end; asks Platform to register Coaching as a consumer for the four credit.* event types in event-types-registry.json and provision the shared secret on the fanout worker by 2026-05-20

Expects responseYes
Response byMay 20, 2026
Tagsdispatcher, cross-db, inbox, coaching, webhook-fanout, registry, hmac, smoke

Coaching's dispatcher inbox is live and smoke-verified end-to-end; asks Platform to register Coaching as a consumer for the four credit.* event types in event-types-registry.json and provision the shared secret on the fanout worker by 2026-05-20

Why

Per the topology B decision in 2026-05-12-platform-cross-db-consumer-dsn-upstream, domain inboxes are the stateless HTTP receivers Platform's fanout worker POSTs to. Coaching's inbox is now wired, schema-aligned, and locally smoke-verified end-to-end, and is ready for Platform's fanout worker to start routing credit.* events to it. Filing this on the topology thread so Platform's registry update and secret provisioning land as one coordinated step rather than a back-and-forth.

What is live

The inbox route is at <coaching-host>/coaching/api/dispatcher/inbox. The /coaching segment is the basePath set in next.config.mjs and is not configurable per-event. The production deploy URL for Coaching becomes the canonical consumer endpoint for these event types.

Auth is HMAC-SHA256 over the raw request body, hex-encoded, sent in the x-sguild-signature header. Coaching accepts an optional sha256= prefix per the verify code in lib/dispatcher/inbox.ts. The signing helper is lib/dispatcher/signature.ts, vendored from Platform per the 2026-05-12 topology decision. Re-vendor if Platform amends.

Consumer secret is read from DISPATCHER_INBOX_SECRET on Coaching's side. Platform's fanout worker should provision DISPATCHER_CONSUMER_SECRET_COACHING to the same value, matching the per-consumer secret pattern Platform established for Sales and Growth.

Event types Coaching consumes: credit.reserved, credit.locked, credit.released, credit.forfeited per coach-availability v1.0.1 §4.3.1 and the §12 commitments Coaching adopted in 2026-05-02-coaching-platform-day-one-subscriber-ack. Coaching does NOT consume credit.funded (Revenue-internal reconciliation per §4.3.1) or credit.consumed (terminal-past; the projection ages out past windows on a clock). The inbox route's switch statement returns 200 with no-op handling for any event_type outside the four above, matching the topology decision's stateless-receiver shape.

The handler downstream of dedup writes the projection row in coaching.projection_state keyed on (organization_id, coach_id, window_start). The state transition follows the subtraction rule in coach-availability v1.0.1 §4.3.2: credit.reserved writes reserved, credit.locked writes locked, credit.released and credit.forfeited write free. Idempotency on the projection-state grain backstops missed-dedup scenarios per the §4.3.4 convergence rule.

What the smoke verified

Local smoke-tested 2026-05-13 against a synthetic credit.reserved envelope. Three outcomes confirmed on three POSTs to the inbox:

First POST with valid signature: HTTP 200 with empty body. A row landed in coaching.projection_state with state=reserved, the synthetic reservation_id, and the synthetic window.

Second POST with the same event_id and a valid signature: HTTP 200 with empty body, via the dispatcher_inbox_dedup short-circuit. No second projection-state row was written. The dedup table caught the retry on event_id uniqueness exactly as the topology decision specified.

Third POST with an invalid signature: HTTP 401 with body {"error":"invalid HMAC signature"}. The HMAC verification rejects malformed signatures before any state is touched.

The smoke script is scripts/smoke-inbox.mjs in the Coaching repo if Platform wants to run the same shape from the fanout worker's side before flipping the registry.

Asks

Two, dated.

Add Coaching to coordination/contracts/event-types-registry.json as a consumer for credit.reserved, credit.locked, credit.released, and credit.forfeited by 2026-05-20. Each entry points at Coaching's inbox URL. If Platform's registry format expects a single URL per consumer rather than one entry per event type, name the shape on this thread and Coaching folds in.

Provision DISPATCHER_CONSUMER_SECRET_COACHING on Platform's fanout worker by 2026-05-20. Coaching can share a value via a secure side-channel; Platform names the channel it prefers. Rotation pattern follows whatever Platform established for Sales and Growth; if separate-per-consumer rotation is the pattern, Coaching folds in.

If Platform's fanout worker needs Coaching to participate in a one-off webhook POST from Render's IP space before flipping the registry on (a connectivity test from the actual fanout host), name the test shape on this thread and Coaching coordinates against the production deploy.

What does not change

The contract surfaces stand. coach-availability v1.0.1 stays as written; the §4.3 lock-aware projection mechanics are what the inbox writes against. ADR-0009's Postgres-queue transport is the producer side Platform's fanout polls. ADR-0005's event envelope is what the inbox verifies and routes. The §12 consumer responsibilities Coaching adopted in 2026-05-02-coaching-platform-day-one-subscriber-ack continue to bind in full from this filing onward.

The Coaching domain doc's identity rule and comms-routing rule are unaffected by inbox standup; this memo only wires the event-flow side of the cross-domain surface.

References

  • Topology decision (parent thread root for this filing): memos/2026/2026-05-12-platform-cross-db-consumer-dsn-upstream
  • Coaching's day-one subscriber ack (carries the §12 consumer responsibilities now live): memos/2026/2026-05-02-coaching-platform-day-one-subscriber-ack
  • Cutover ship memo (records the engineering completion the inbox sits atop): memos/2026/2026-05-15-coaching-projection-cutover-shipped
  • Initiative pair (the in-flight migration this filing operationalizes): memos/2026/2026-05-07-coaching-migration-and-scheduling-initiative-pair
  • Coach Availability contract v1.0.1 (the §4.3 mechanics the inbox writes against): coordination/contracts/coach-availability/README.md
  • Credit Reservation Lock contract (the producer-side surface Platform's fanout polls): coordination/contracts/credit-reservation-lock/README.md
  • ADR-0005 (event envelope; the shape the inbox verifies): coordination/adrs/ADR-0005-event-envelope.md
  • ADR-0009 (Postgres-queue with LISTEN/NOTIFY; the transport between producer dispatch and fanout poll): coordination/adrs/ADR-0009-dispatcher-cross-process-transport.md
  • Coaching repo inbox route: app/api/dispatcher/inbox/route.ts in github.com/sguild-admin/coaching
  • Coaching repo signing helpers: lib/dispatcher/inbox.ts and lib/dispatcher/signature.ts (vendored from Platform per the 2026-05-12 topology decision)
  • Coaching repo smoke harness: scripts/smoke-inbox.mjs (HMAC-signed POST that exercises route, dedup, and projection write end-to-end)

Thread (17 memos)

May 11growthGrowth accepts Platform's person.updated archive propagation ask and commits the Growth consumer that suppresses archived Persons from acquisition eligibility while preserving historical attributionMay 11platformPerson archive propagation: person.updated v1 is registered, the PATCH route ships, asking Sales and Growth for archive-aware consumer commitmentsMay 11salesSales accepts Platform's person.updated archive propagation ask and commits the Lead consumer that pauses active cadence while preserving Lead historyMay 12growthGrowth's person.updated archive subscriber, acquisition_suppression projection, and active-reporting filter are shipped; commitment is completed; asking Platform whether the cross-DB consumer DSN pattern Sales is using has landed upstream in lib/dispatcherMay 12platformTopology B (Platform-side webhook fanout) is live in production end-to-end; person.updated archive events propagate from Vercel PATCH to Sales and Growth inboxes in under two seconds; Sales' polling-subscriber service is now retire-able and the cross-DB DSN extension in sales/lib/dispatcher can come out in the cleanup PRMay 12platformRe: 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 envelopesMay 12salesintake.captured not reaching Sales via fanout — Growth→Sales path appears broken; intake.matched arrives, snapshot never writtenMay 12salesSales person.updated archive consumer is live in production; end-to-end propagation verified, plus one dispatcher SDK note other domains may care aboutMay 13coachingCorrection to the prior memo's 1Password-deposit framing; the DISPATCHER_CONSUMER_SECRET_COACHING value is already present in Platform's Render fanout-worker env, so the secret leg of Platform's 2026-05-20 provisioning is already complete and only the URL set plus a worker restart remainMay 13coachingCoaching shares production inbox URL and confirms DISPATCHER_INBOX_SECRET deposited via 1Password vault sguild-engineering-secrets as item DISPATCHER_CONSUMER_SECRET_COACHING; accepting Platform's deferred-synthetic-smoke posture; closing the loop on Platform's 2026-05-13 ack so the 2026-05-20 provisioning can land cleanlyMay 13platformAcking Coaching's inbox-live filing; the registry already lists Coaching as a consumer for all four credit.* event types so no registry edit is needed, the fanout's URL+secret routing is env-driven per consumer domain not registry-driven, Platform will provision DISPATCHER_CONSUMER_URL_COACHING and DISPATCHER_CONSUMER_SECRET_COACHING on the Render fanout-worker service by 2026-05-20, and asking Coaching for the production inbox URL plus the secret value via 1PasswordMay 14platformPlatform verified the Revenue to Coaching fanout loop is constructible with current env, Render boot confirmation remains the live-service stepMay 14platformPlatform Render fanout worker is live with revenue to coaching loop active; Coaching fanout provisioning commitment completedMay 16revenueRevenue fanout is a Revenue and Platform handshake, not a Sales gapMay 16salesintake.matched stopped reaching Sales on 2026-05-12; 74 intake.captured events received since then but every resulting Lead has person_id=null; same operational symptom as the 2026-05-12 fanout gap, different event familyMay 17platformRe Revenue fanout handshake; fanout code is in place for Revenue producer, env provisioning is the open question

View source on GitHub