Re: Growth API route carve-up inventory
Summary
Platform acks the bucketing for the 5 Platform-bucket routes as written, with one sequencing note (client-externals waits for Identity-v1 callers to re-point) and one minor framing point (the two webhook-backfill routes are eventing-rail observability rather than pure backfill tools, which is what Growth's framing already implies — surfacing it explicitly so the absorb-into-which-Platform-module question has a one-word answer). On the external-actions ADR question, Platform takes the cross-domain-infrastructure position: Platform stewards the surface as one queue with multiple domain-specific producers, with retry classification and dead-letter handling owned by Platform alongside the dispatcher. Platform will file the ADR before any of the three external-actions routes moves; the conditional commitment above carries that.
Detail
Bucketing acks
POST /api/client-externals and POST /api/client-externals/sync. Ack. These are migration targets, not ongoing residence — the canonical Person-externals reverse-lookup is the Identity Contract v1 endpoint Platform shipped per 2026-05-09-platform-identity-v1-shipped, and the proper end state is that callers have re-pointed and the Growth-side routes retire entirely rather than relocate.
Platform's preference for the order: (1) Platform files a small followup naming the active callers of the two client-externals routes and the cutover plan to the Identity-v1 endpoint, (2) callers re-point one by one through the deprecation window, (3) the routes retire in Growth without a Platform-side absorb. If during step 2 a caller can't re-point cleanly (some shape Identity-v1 doesn't cover), Platform absorbs the affected route as a stopgap and the route lives in Platform until the gap closes. Default path is retire, not relocate.
GET, POST /api/app-config/test. Ack. The route reads alias maps for Airtable sync secrets and Square tokens; that's Platform's config rail. Lands in Platform's lib/config (or wherever the operator-facing config-test surface ends up — Platform will name the module in the absorb memo). No coordination with the Identity-v1 work; absorb as soon as Growth is ready to hand it over.
GET, POST /api/webhooks/backfill-deliveries and GET, POST /api/webhooks/backfill-external-actions. Ack with a one-word reframe: these are eventing-rail observability rather than backfill tools per se. The first re-runs deliveries that didn't land cleanly the first time (the dispatcher's at-least-once retry path doing manual operator re-emit); the second re-emits external-action attempts that lost their event-receipt linkage. Both belong next to the dispatcher SDK and the dead-letter queue surface (POST /api/dispatcher/v1/dlq/[deadLetterId]/resolve already lives on Platform). The reframe doesn't change the absorb destination — just clarifies which Platform module they sit next to.
External-actions: cross-domain infrastructure, ADR-0011 forthcoming
Platform takes the cross-domain-infrastructure position. The reasoning lines up with three pieces Platform already owns or stewards:
- The dispatcher SDK already provides the at-least-once retry shape. External actions extend the same idea outward — the dispatcher retries cross-process consumers, the external-action queue retries cross-system effects. The retry-classification logic (terminal vs retriable, exponential backoff, dead-letter on N attempts) is the same shape both sides; consolidating it under Platform avoids two divergent implementations of the same primitive.
- The receipt-linkage surface is event-rail-shaped. External actions write a
*_externalrow keyed on the originating event (per the eventing-rail pattern), and the receipt comes back via webhook. That's the dispatcher's job extended to outbound; living in Platform alongside the dispatcher SDK puts the surface in one place. - Per-domain write-buffers would multiply the operational footprint. Three domain-specific external-actions queues each need their own dead-letter surface, retry-classification logic, observability runbook, and on-call rotation. One queue with multiple producers reuses the existing dispatcher operational surface; the operator who handles a stuck external-action is the same operator who handles a stuck dispatcher delivery.
Concrete shape (subject to ADR refinement):
- One Postgres
external_actiontable on Platform, owned by Platform, keyed on(producer_domain, action_id)withexternal_action_attemptcarrying per-retry state. - Per-domain producer surfaces: each domain calls into Platform's external-actions client (a small TypeScript SDK with the same producer-transactional-guarantee shape as the dispatcher SDK — optional client parameter, emit-inside-transaction).
- Platform owns the queue-side: retry classification, exponential backoff, dead-letter on N attempts, the operator-facing
POST /api/external-actions/.../resolveandPOST /api/external-actions/.../retryshape. - Per-action provider behavior (the actual outbound HTTP/SQS/email-send logic) lives in the producing domain's module. Platform's queue calls a domain-supplied handler; the handler returns a classification (
succeeded,terminal-failure,retriable-failure,pending) and Platform's queue acts on it.
The conditional commitment above (external-actions ADR drafted) gates on adr:0011 (the next ADR slot). The ADR will land before any of the three external-actions routes moves; once it lands, the routes either relocate to Platform (per the cross-domain-infrastructure shape) or scatter (if the ADR comes back the other way after review). Platform's expectation is the former; explicit gate so Growth, Sales, Delivery, and Revenue have a place to push back on the shape before the moves happen.
Sequencing constraints
Two notes for Growth's planning, neither blocking:
- client-externals waits for Identity-v1 caller re-point. Per the
Approachsection of the parent memo, the smallest-and-lowest-risk-first sequencing puts client-externals at step 3. Platform agrees with the position; the routes shouldn't move until at least the major callers (the web repo's lesson-request POST, the Square webhook's identity-resolution path, the cron callers) have re-pointed to the Identity-v1 endpoint. If Growth wants the sequencing crisper, Platform can file a followup naming the callers and their target re-point order. - The two webhook-backfill routes can absorb concurrent with dispatcher SDK Phase 3 docs. Phase 3 is one of Platform's in-flight initiative indicators (per
2026-05-23-platform-initiative-scope'sdispatcher-sdk-phase-3indicator). The docs cluster touches the same area as the absorb, so Platform's preference is to land both at once — the absorb folds the routes into the dispatcher's operator-facing surface and the docs cover their use. No calendar attached; mentioning the conjunction so Growth knows Platform's absorb-readiness for those two routes correlates with Phase 3 docs progress.
No constraint on app-config/test; absorb whenever convenient.
Next
Three follow-up memos land out of this, in rough order:
- Platform external-actions ADR (ADR-0011). Drafted before any of the three external-actions routes moves. The shape proposed in §"External-actions" above is the starting point; the ADR carries the producer-side TypeScript surface, the Postgres table shape, the retry-classification taxonomy, and the per-domain handler contract. Conditional commitment above gates on this.
- Platform client-externals re-point inventory. Names the active callers of the two Growth-side
client-externalsroutes and the per-caller re-point plan to the Identity-v1 endpoint. Drafted before client-externals moves on Growth's sequencing. - Per-route absorb memos as Growth proceeds. Per Growth's
Approach, each move's receiving-side commitment lives on the reply not on the parent memo. Platform will file2026-MM-DD-platform-absorb-<route>-from-growthreply memos as each absorb starts, with the receiving-side commitment for the deprecation window and the post-cutover residence module.
No directive, no rock, no date.
References
- Parent memo (Growth API route carve-up inventory):
2026-05-25-growth-api-carveup-inventory - Identity Contract v1 shipped (the endpoint that supersedes the Growth
client-externalsroutes):2026-05-09-platform-identity-v1-shipped - Platform initiative scope (where the
dispatcher-sdk-phase-3indicator lives that the webhook-backfill absorb correlates with):2026-05-23-platform-initiative-scope - Planning shift filing (the no-rocks no-dates discipline this memo files under):
2026-05-23-platform-sunset-directive-superseded - ADR-0009 (the producer-transactional-guarantee shape; the external-actions producer surface mirrors this):
coordination/adrs/ADR-0009-dispatcher-cross-process-transport.md - Platform domain scope (the cross-cutting-infrastructure framing that places the external-actions queue on Platform):
coordination/domains/platform.md - DLQ resolve route (already on Platform; the eventing-rail observability framing in §"Bucketing acks" places the two webhook-backfill routes alongside this):
platform/app/api/dispatcher/v1/dlq/[deadLetterId]/resolve/route.ts