Skip to content

ADR-017: Platform v2 — Substrate Adoption, Framework Deferred

Status

Accepted (2026-05-20). Reduces the scope of features/platform-v2/spec.md and supersedes the multi-phase rollout described in features/platform-v2/plan.md.

Context

PR #701 completed Phase 1 of platform-v2 in a single day of ralph-driven authoring: 32,507 insertions across 404 files. All 100 plan items checked off; validation gate green. The spec called for six phases:

  1. Framework + dep upgrades + ESM flip + 10 reference routes (this PR)
  2. Bulk migration of ~300 remaining v1 routes
  3. Full frontend rewrite
  4. Edge layer migration
  5. Cutover (Stripe/Supabase URL changes)
  6. Docs

The PR has been sitting in DIRTY state against main while feature work continued shipping. The decision in front of us was: land Phase 1 as-is and continue toward Phase 2, or change course.

The calculus that drove the original plan no longer holds

The plan was written when each phase looked like weeks of human authoring. Frontier-model velocity has collapsed authoring cost — Phase 1 shipped in a day. That changes one variable only: the cost of writing code.

It does not change:

  • Cost of an incident on a money path
  • Cost of human verification on 300 routes
  • Reviewer cognitive load when v2 is the substrate at 2am during a Stripe outage
  • Customer trust when checkout 500s

The traditional argument against rewrites — "they starve features for months" — dies at light speed. The other argument against rewrites — "you replace working code with code you don't understand yet, and human verification doesn't get faster" — gets worse at light speed. The human-reviewer-to-machine-output ratio collapses.

Metrognome is one developer working alongside Claude. v1 routes are not bottlenecked on authoring quality. They are bottlenecked on the things v2 doesn't fix: customer acquisition, conversion, retention, ops leverage.

What's actually in PR #701

The PR bundles three logically separate bodies of work:

  1. Substrate (Block A, items 1–14 + fallout): Node 22→24, TS 5.7→6, Stripe SDK 18→22, Twilio 5→6, Vitest 3→4, Tailwind 3→4, pnpm 8→11, ESM flip, ~99 test files migrated for vitest 4, 32 email templates migrated for Tailwind 4, 4 unused deps removed.
  2. Framework (Blocks B–L): lib/http/ (defineRoute, predicates, output kinds, error model, observability, codegen, lint rules), IdempotencyKey table, service signature adaptations across 14 services (StripeService, ReservationService, FinanceService, UserService, etc.).
  3. 10 reference routes (Block M): contactForm, getReservation, docsAsk, oauthCallback, exportFinance, syncStripeCron, stripeWebhook, and a few others.

Block 1 had to happen eventually regardless of v2 — Node 22 EOL, Stripe SDK deprecations, Tailwind 3 sunset. Blocks 2 and 3 are net-new infrastructure that pay off only if Phase 2's bulk migration happens.

Decision

Land substrate, defer framework

Split PR #701 into two sequential PRs against main:

  • PR-A — Substrate. Cherry-pick Block A (items 1–14) plus dep-upgrade fallout commits (Vitest 4 mock migration, Tailwind 4 email migration, Stripe mock factory fixes, postinstall fix, async-mock timing fix, Turnstile abort signal). v1 behavior preserved; only the runtime/build substrate changes. Land first, soak in production 1–2 weeks.
  • PR-B — Framework + 10 routes. Everything else: lib/http/, predicates, output kinds, codegen, lint rules, service restructures, IdempotencyKey migration, the 10 reference routes. Lands only after PR-A is proven stable in production. May never land — see "framework deferred" below.

This split buys bisectable incidents (Stripe 22 vs StripeService restructure is distinguishable), an independent rollback path, and an off-ramp for the framework if substrate verification surfaces enough surprises.

Bulk migration is killed

Phase 2 (bulk migration of ~300 routes) is explicitly cancelled. Not deferred. Not paused. Cancelled.

Rationale: at light-speed authoring, the marginal cost of migrating a route is small, but the marginal verification cost is unchanged. 300 routes × human verification × money-path blast radius = not a project worth running, especially at one-dev scale. The bulk-migration framing also creates momentum pressure to migrate routes the team doesn't currently need to touch — work that produces no customer value.

Frontend rewrite is killed

Phase 3 (full frontend rewrite beyond the 10 hooks regenerated for the reference routes) is cancelled. No customer pain justifies it. Historical track record of single-dev frontend rewrites is brutal.

Phases 4–6 are killed

Edge-layer migration, cutover, and full v2 docs are cancelled as planned units. If the framework is adopted opportunistically and 60%+ of routes migrate naturally over time, revisit. Otherwise leave alone.

Adoption policy (assuming PR-B lands)

Route class Policy
New routes (greenfield) Use v2 (defineRoute). Lint enforces.
Touched routes (already opening for a feature) Migrate if cheap. Otherwise leave on v1.
Money paths (Stripe webhooks, checkout, payment intent creation, refund flows) Stay on v1 indefinitely. Migrate one at a time only with explicit, deliberate verification, never as part of a sweep.
Webhooks (Twilio, Supabase, third-party) Stay on v1 indefinitely. Same reasoning.
All other v1 routes Stay on v1 indefinitely.

Coexistence is permanent, not transitional. v1 and v2 patterns will live side-by-side for the foreseeable future. The framework pays for itself on greenfield work; it does not require migration of existing routes to justify its existence.

Lint rules in PR-B must not flag v1 patterns codebase-wide. They enforce v2 patterns on v2 routes only. Verify this before merging PR-B.

Spec and plan

Update features/platform-v2/spec.md to reflect that v1 patterns remain canonical for the routes that use them. Mark features/platform-v2/plan.md as "Phase 1 complete; Phases 2–6 cancelled per ADR-017."

ADR-014 (API response standardization) and ADR-009 (service architecture) remain canonical for v1 routes. v2 supersedes both within v2 routes only.

Consequences

Positive

  • Substrate work (deps + ESM) lands and pays off immediately. No upgrade debt accumulating.
  • Framework option is preserved without committing to its full rollout. Greenfield routes get the better pattern; legacy routes get left alone.
  • The 10 reference routes serve as templates for new work without forcing migration.
  • Frees Aaron + Claude to redirect light-speed velocity to customer-facing work: growth experiments, conversion improvements, retention flows, ops automation.
  • Eliminates the implicit pressure of a partially-migrated codebase. There is no "we should finish Phase 2" looming.
  • Risk surface per landing event is small enough to verify by hand.

Negative

  • Two architectural patterns coexist forever. Cognitive tax on Aaron and on Claude when reading the codebase. Mitigated by the fact that Claude can write either pattern fluently, and the v1/v2 boundary is visible in imports (@/lib/http/... vs @/lib/api/...).
  • Investment in framework primitives that aren't used by 97% of routes. Real, but already sunk by Phase 1.
  • v1 service restructures (Block K) in PR-B touched 14 services including money paths. Verifying these in isolation is the riskiest part of the remaining work and the most likely reason PR-B never lands.
  • "Opportunistic migration" risks drifting back into ad-hoc bulk migration if not actively resisted. Mitigation: this ADR is the load-bearing decision. When in doubt, leave v1 alone.
  • Removes the symmetry the spec was reaching for. The codebase will not be "clean" by any standard rewrite-driven definition. That is the correct tradeoff.

Operational

  • PR #701 is closed without merging once PR-A and PR-B (or just PR-A) supersede it.
  • The Block K service restructures (StripeService, ReservationService, FinanceService, SubscriptionService, UserService, ContactService, SecurityService, WaitlistCheckoutService, ReservationSideEffectService, MgIdPresence integration test, SubscriptionCreateHandler, StripeWebhookService) only ship with PR-B. If PR-B is abandoned, those restructures are abandoned with it. v1 services stay as they are on main today.
  • The IdempotencyKey table migration ships with PR-B, not PR-A.
  • The platform-v2 gate workflow (.github/workflows/platform-v2-gate.yml) ships with PR-B.
  • ralph workspace cleanup (the ralph/platform-v2 branch) happens after both PRs land or after PR-B is explicitly abandoned.

References