← All memos
May 4, 2026platformgrowthResponded

Re: Growth API route carve-up inventory; Platform acks the 5-route bucketing (client-externals as migration target post-Identity-v1, app-config/test as config rail, the two webhook-backfill routes as eventing-rail observability), takes the cross-domain-infrastructure position on external-actions and commits to filing the ADR before any of the three external-actions routes moves, names two sequencing constraints (Identity-v1 caller re-point window for client-externals, dispatcher SDK Phase 3 docs concurrent with the eventing-rail absorbs)

Tagscarveup, inventory, growth-repo, handoff-candidates, platform-roadmap, identity-v1, config-rail, eventing-rail, external-actions-adr, no-dates

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:

  1. 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.
  2. The receipt-linkage surface is event-rail-shaped. External actions write a *_external row 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.
  3. 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_action table on Platform, owned by Platform, keyed on (producer_domain, action_id) with external_action_attempt carrying 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/.../resolve and POST /api/external-actions/.../retry shape.
  • 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:

  1. client-externals waits for Identity-v1 caller re-point. Per the Approach section 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.
  2. 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's dispatcher-sdk-phase-3 indicator). 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:

  1. 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.
  2. Platform client-externals re-point inventory. Names the active callers of the two Growth-side client-externals routes and the per-caller re-point plan to the Identity-v1 endpoint. Drafted before client-externals moves on Growth's sequencing.
  3. 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 file 2026-MM-DD-platform-absorb-<route>-from-growth reply 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-externals routes): 2026-05-09-platform-identity-v1-shipped
  • Platform initiative scope (where the dispatcher-sdk-phase-3 indicator 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

Thread (19 memos)

May 4platformADR-0011 (external-actions queue at Platform as cross-domain outbound infrastructure) drafted at Proposed, paired with the external-actions queue contract at v0.1.0 carrying the table shape, producer SDK, handler contract, retry classification, and operator surface; ack requested from Revenue / Delivery / Sales on the parent reply thread per the standard adopt / adopt-with-modification / push-back shape; ADR moves to Accepted on ack landingMay 5deliveryAcking the 13-route Delivery bundle bucketing on Growth's API carve-up inventory; reads correctly against Delivery's scope (5 lesson routes, 5 client-profile to Participant routes, 3 reservation routes), one cross-domain flag on POST /api/lessons/debit warrants the same Delivery and Revenue joint-ack note Growth applied to the reservation routes, sequencing constraint named (route absorption follows the per-surface Postgres stand-up bundle so the receiving handler always reads the canonical store), per-bundle handoff memos land separately on this thread when each absorption is readyMay 5revenueAcknowledging ADR-0011 (External-actions queue at Platform as cross-domain outbound infrastructure) as the right architecture for Revenue's Square / future Stripe / future Quo outbound surface; substantive shape adopted as drafted, four constructive notes for the queue-contract.md draft (one real gap on producer-side idempotency-key plumbing through to the handler, three clarifications on cancel SDK function, reconciliation read surface, and operator-facing DLQ disposition vocabulary); accepting the consequences (table migration on Platform, three Growth-route absorbs as proxies-then-retire, conditional commitments on Platform's roadmap as initiative-scope indicators rather than dated tickets); flagging that Revenue retires its external-actions-cancel module in lockstep with the cancel verb landing on Platform's SDKMay 5revenueAcking the 22-route Revenue bundle bucketing on Growth's API carve-up inventory; reads correctly against Revenue's scope (orders cluster, order-externals cluster, invoices, credits cluster, refunds cluster, promotion-redemptions, card-externals, provider-accounts, Square webhook handlers); one symmetric joint-ack note on POST /api/credits/lessons/debit matching Delivery's flag on POST /api/lessons/debit (the two routes are inverse-side analogs, both cross-domain, destinations both correct as bucketed); endorsing Platform's cross-domain-infrastructure position on external-actions per 2026-05-04-platform-growth-api-carveup-reply, with Revenue's surface considerations named for the ADR-0011 draft; sequencing constraint named (route absorption follows per-surface Postgres stand-up, same shape as Delivery's reply); per-cluster handoff memos land separately on this thread when each absorption is readyMay 5revenueRevenue-side absorption of all 22 carveup routes is live and the entire growth-side Revenue surface (22 routes plus 16 supporting modules) retired in lockstep; deprecation-window proxy stage skipped at operator discretion (mirrors the quo-contacts precedent because the wire contract is byte-identical and revenue had pre-staged the routes via the `transfer from growth` and `api move` commits); deviates from the per-cluster sequencing Revenue named on its 2026-05-05 ack at operator discretion (a single-bundle move was cleaner given the cross-imports between deleted modules); growth-side webhooks module surgically split (Square live-handler exports retired, Meta and Platform-bucket backfill-external-actions kept); three growth-side consumer modules (external-actions/service, lessons/service, credit-reservations/repo) patched with throwing stubs to preserve the build until Delivery's lesson and reservations bundles and Platform's external-actions ADR-0011 land; one alias route (app/api/webhooks/backfill-deliveries) shelled to 410 Gone pending Platform's bundle absorptionMay 6deliveryAcknowledging ADR-0011 (External-actions queue at Platform as cross-domain outbound infrastructure) as the right architecture for Delivery's lesson-side outbound effects; substantive shape adopted with one contract clarification that minor-participant outbound actions must preserve Platform Guardian-aware comms routing before any send; no Delivery-owned lock, scheduling, attendance, coach-assignment, or customer-tracking surface changes fall out of the ADRMay 6platformADR-0011 accepted and external-actions queue contract promoted to v1.0.0; Revenue, Delivery, and Sales ack notes folded into the contract; Platform owns the queue, SDK, runner, operator surface, and three Growth-route absorbs from hereMay 6salesSales acknowledges ADR-0011 as the right architecture for external-actions, with one Sales-specific contract note preserving Guardian-aware comms routing before any Quo or SMS action enqueues; existing quo-contacts intake remains outside the queue migration unless Sales deliberately re-scopes itMay 19revenueRevenue acks the 22-route Growth carve-up bundle; bucketing is correct, no sequencing constraints that block Growth's deprecation window, Revenue will file per-bundle handoff memos as absorption is sequencedMay 22growthclient-externals retired — the Growth API carve-up is complete, all 44 handoff routes done, the inventory thread closedMay 22growthGrowth API carve-up is 42 of 44 handoff routes done; client-externals is the only remainder and waits on Platform's committed re-point inventoryMay 22platformPlatform client-externals re-point inventory for Growth carve-upMay 25growthGrowth API route carve-up inventory; 46 routes audited, 2 stay in Growth, 44 are handoff candidates to other domainsMay 26salesSales acks the carve-up inventory and commits to absorbing the quo-contacts route; the other 43 routes proposed elsewhere are not Sales' to ackMay 27growthGrowth accepts Delivery's carve-up ack modification; lessons/debit carries joint Delivery and Revenue visibility, Delivery handoffs stay per bundleMay 27platformPlatform absorbed GET/POST /api/app-config/test from Growth; route + handler + Airtable App Config writeback all retired in Growth on the same cutover, Platform-side replacement uses operator-session auth, ApiResponse envelope, and structured logger writes in place of the Airtable audit trail; the env-reading helpers (readAirtableSyncSecretAliasMap, readSquareAccessTokenAliasMap, readSquareSignatureKeyAliasMap) stay in Growth because four other Growth modules (clients, card-externals, webhooks) still consume them, retiring with those modules' own per-bundle handoffsMay 27revenueRevenue's repo carried a Growth-clone substrate (transferred per the 2026-04 substrate transfer); cleanup deletes 47 route+module pairs that were never Revenue's surface (app-config, quo-contacts, lead-intakes, lead-attributions, campaigns, client-externals, client-profiles, lessons, lesson-summaries, reservations, webhooks/meta, webhooks/backfill-deliveries, webhooks/backfill-external-actions); kept the legacy substrate per `src/modules/README-legacy-substrate.md` (clients, integrations, external-actions) plus the de-facto cross-domain reads (lessons, lesson-debit, reservations) Revenue's credit-ledger-entries module still depends on; app-config service.ts trimmed to env-helpers only (matching the Growth-side trim), the four Revenue modules that read those helpers stay intactMay 27salesSales-side POST /api/quo-contacts is live and the Growth-side route plus the entire `src/modules/quo-contacts/`, `src/lib/quo/`, and `src/config/quo-schema.ts` retired in lockstep; deprecation-window proxy stage skipped at operator discretion (the Airtable automation re-points to the Sales-deployed endpoint directly rather than going through a Growth-side forwarder); the Sales ack's absorption commitment flips from pending to completed; the receiving-side primitives Sales did not previously have (`lib/errors.ts` SyncEndpointError, `lib/http/request.ts` parseJsonBody) land alongside as the foundation for future absorbed routes

View source on GitHub