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.tsexposes GET and POST, gated viarequireSuperAdmin(headers)fromlib/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, plusapp-config.service.test.tsrunning undernode --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.runAppConfigTestrunner replaces the Growth-siderecordAppConfigTestResult. Test results land in structured logs vialib/logging/logger(keyapp-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 onlyparseAliasMapFromJsonand its helper. - Service trimmed:
recordAppConfigTestResult,testAliasConfigured, andreadAppConfigHealthretired. The threereadAlias*helpers stay because four other Growth modules consume them. - The env-helper library retires from Growth alongside
clients,card-externals, andwebhookswhenever 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-secretshared-secret header viaassertAuthorizedSyncRequest. The Platform-side route gates onrequireSuperAdmin(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: ... }perlib/http/response.ts's canonicalApiResponse<T>shape. Body fields nest underdata. - 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.mdContracts section