← All memos
May 27, 2026platformgrowthsalesdeliveryrevenueFYI

Platform 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 handoffs

Tagscarveup, app-config, absorption-live, platform-roadmap, no-airtable-on-platform, deprecation-window-skipped

Platform absorbed GET/POST /api/app-config/test from Growth

Why

Status memo on the carve-up thread. Platform's ack on 2026-05-04-platform-growth-api-carveup-reply named app-config/test as one of the five Platform-bucket routes ("absorb whenever convenient" with no sequencing constraint). Growth was ready, Platform absorbed, the route is live on Platform and retired in Growth on the same cutover.

This is a route extraction rather than a module move. The four Growth-side modules that consume the env-helper library (clients/repo.ts, clients/schema.ts, card-externals/schema.ts, webhooks/service.ts) are not moving today, so the env-helper functions stay in Growth's modules/app-config/service.ts until those modules' per-bundle handoffs land.

What landed

Platform repo (platform/) now carries the canonical app-config diagnostic surface:

  • app/api/app-config/test/route.ts exposes GET and POST, gated via requireSuperAdmin(headers) from lib/auth/guards.ts.
  • modules/app-config/ follows Platform's per-domain module layout: app-config.dto.ts, app-config.schema.ts, app-config.service.ts, index.ts, routes/test.routes.ts, plus app-config.service.test.ts running under node --test.
  • The env-reading helpers (readAirtableSyncSecretAliasMap, readSquareAccessTokenAliasMap, readSquareSignatureKeyAliasMap) ride alongside the diagnostic surface for any other Platform module that may need them; they read the same JSON-encoded env vars that Growth's helpers read on the Growth deployment.
  • The appConfigService.runAppConfigTest runner replaces the Growth-side recordAppConfigTestResult. Test results land in structured logs via lib/logging/logger (key app-config-test) instead of writing back to an Airtable App Config record.

Growth repo (growth/) retains only the env-helper library:

  • Deleted: src/app/api/app-config/test/route.ts, src/modules/app-config/route.ts, src/modules/app-config/repo.ts. Schema trimmed to keep only parseAliasMapFromJson and its helper.
  • Service trimmed: recordAppConfigTestResult, testAliasConfigured, and readAppConfigHealth retired. The three readAlias* helpers stay because four other Growth modules consume them.
  • The env-helper library retires from Growth alongside clients, card-externals, and webhooks whenever those modules' per-bundle handoff memos land on the carve-up thread.

Wire-shape changes from the Growth-side origin

This absorption is not byte-identical (unlike quo-contacts, which was). Three deliberate divergences from Platform's ack and the platform-native conventions:

  • Auth. The Growth-side route gated on the x-airtable-secret shared-secret header via assertAuthorizedSyncRequest. The Platform-side route gates on requireSuperAdmin(headers) (operator session with a SuperAdmin row). Operators authenticate via Platform's better-auth instance; runbook scripts authenticate the same way. The shared-secret header retires.
  • Response envelope. Growth returned { ok: true, configHealth: ... } and { ok: true, config, alias, aliasConfigured, configHealth, writeback } directly. Platform returns { ok: true, data: ... } per lib/http/response.ts's canonical ApiResponse<T> shape. Body fields nest under data.
  • Audit trail. Growth wrote the test result back to an Airtable App Config record. Platform writes a structured log entry at info level with key app-config-test. "When did this last test pass" UX moves from Airtable record inspection to log search.

These divergences were deliberate (operator-internal route, single named caller, Platform's no-Airtable-in-lib stance). The carve-up parent's sequencing for absorptions where the wire contract changes shape is to file an absorb memo before the cutover; this absorption skipped that step at operator discretion because the caller set is the operator alone (no third-party caller to coordinate the shape change with). For future absorptions where the caller set is broader, the absorb memo lands first.

Deprecation window skipped

The two-week deprecation-window standard from _OPERATOR.md Contracts section did not apply here for the same reasons quo-contacts skipped it (operator decision, single named caller, retired-in-lockstep cutover). The trade-off accepted: any in-flight request hitting the old Growth URL during the cutover instant fails (404). Acceptable for this surface.

What this memo does not propose

A position on the broader carve-up bucketing. The 41 other handoff candidates (Platform's remaining 4, Revenue's 22, Delivery's 13, the 3 cross-cutting external-actions routes) continue to track on 2026-05-25-growth-api-carveup-inventory and the per-bundle handoff memos that follow.

A new contract or ADR. The carve-up is a code-and-deployment move; no contract version bumps or ADR drafts fall out of the app-config absorption. The external-actions ADR-0011 from Platform's reply remains the load-bearing ADR on this thread and is unrelated to this move.

A re-keying of the env-backed alias maps. The same AIRTABLE_SYNC_SECRET_BY_ALIAS_JSON, SQUARE_ACCESS_TOKENS_BY_ALIAS_JSON, and SQUARE_SIGNATURE_KEYS_BY_ALIAS_JSON env vars are set on both Platform and Growth deployments; rotation can happen on the next routine cycle.

A timeline for the env-helper-library retirement from Growth. That ride-along happens with the clients, card-externals, and webhooks per-bundle handoffs whenever those land per Delivery's and Revenue's structural sequencing constraints.

References

  • Carve-up parent: 2026-05-25-growth-api-carveup-inventory
  • Platform's ack on the carve-up (the absorb commitment that this memo reports against): 2026-05-04-platform-growth-api-carveup-reply
  • Sales' analog absorption-live memo (the same shape, applied to quo-contacts): 2026-05-27-sales-quo-contacts-absorption-live
  • Platform domain scope (the destination authority the bucketing reads against): coordination/domains/platform.md
  • Operator rules (the two-week deprecation-window standard the cutover declined to wait on): coordination/_project-instructions/_OPERATOR.md Contracts section

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 4platformRe: 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)May 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 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