BigQuery→Postgres serving migration (per-page, fallback-guarded, parity-gated)
Directed and reviewed a page-by-page migration of a multi-tenant reporting product's reads from BigQuery to Postgres, executed by a Claude Code agent under a fixed five-deliverable contract. A single read chokepoint (readVia) decides the engine and falls back to BigQuery with a loud log; a non-throwing predicate feeds it while a throwing gate protects unconverted sites, and a module-load guard refuses to enable a partially-converted page. Parity is proven twice, as owner (is the copy faithful?) and, critically, from a scoped editor session (is each tenant's subset correct?) after discovering the harness was structurally blind to per-scope narrowing. Every flip passed exact parity, zero BigQuery jobs in a healthy render, real in-function round-trip timings (20-34 ms), and an end-to-end fallback exercise. Conversions and CRM converged to 420/684 and 389/659 ms; the outlier Overview Metrics was attributed (render 80 ms, ~13 round-trips) and its residual handed to Phase 2B.
Parity had to be measured from the losing side, a superadmin is the worst possible witness for a scope bug.
Role: Architecture reviewer and decision owner directing a Claude Code agent ('CC') that implemented the migration
The Problem
Client-facing dashboards served their reads from BigQuery, making pages slow (an Overview Metrics page sat well above its floor) and expensive. The move to Postgres-backed serving had to accelerate pages without ever showing a client a wrong number or leaking one tenant's data to another.
Route production reads from BigQuery to Postgres behind a per-page flag while preserving Row-Level Security tenant isolation, guaranteeing exact data parity, counting zero BigQuery jobs in the render, and keeping an automatic fallback, all measured against a co-located latency 'floor'.
Live multi-tenant production; RLS FORCE must hold; no downtime; and a strict 'nothing silently wrong' culture surfaced several one-line-away failures: a react-hooks/rules-of-hooks build failure that tsc missed, a throwing gate inside the read path that was one line from turning every request into a silent BigQuery fallback, a session reaper that killed the load-runner's token before the run started, a parity harness structurally blind to per-scope narrowing, and a 1-ULP float serialization diff from PostgREST.
Approach & Architecture
Advised and reviewed a page-by-page migration behind an additive SERVING_PAGES flag, with a single read chokepoint (readVia) that falls back to BigQuery on error and logs loudly, a module-load guard (PAGES_WITH_FALLBACK) that refuses to enable a page whose read sites aren't converted, and a two-witness parity model. Each page flip had to pass five deliverables: exact parity (owner + scoped witnesses), convergence to the measured page floor from a co-located k6 runner, zero BigQuery jobs in a healthy render, real in-function round-trip timings, and an end-to-end fallback exercise.
readVia(page, read, fromServing, fromBigQuery) is the only place that decides the engine; isServingActive(page) is a non-throwing predicate used by readVia, while servingActiveFor(page) throws for any unconverted 'bare gate' site. A per-page additive flag (SERVING_PAGES) plus a PAGES_WITH_FALLBACK set enforce that an enabled page has all its sites converted. Postgres serving.* uses SECURITY INVOKER RPCs so per-table RLS applies; a consolidated RPC LEFT-JOINs annotations onto an inventory spine. verify-serving-parity runs as owner (faithful copy), verify-scoped-parity runs as a scoped editor (correct subset), verify-tenant-isolation covers widening via a fixture. A k6 scenario in us-east1 mints a throwaway internal session per run and reaps it in a finally/trap to measure the floor.
Key Decisions & Trade-offs
Hardest Part
Discovering that the parity harness itself was blind: verify-serving-parity ran as owner (RLS bypassed) and verify-overview-metrics as superadmin, so a policy that NULLed an annotation for a scoped user would pass green. Fixed by adding verify-scoped-parity measured under a scoped editor session and requiring the consolidated LEFT-JOIN RPC to return annotations identical to the separate RPCs for that same session.
Technical Detail
serving.* tables with RLS FORCE and INVOKER RPCs (serving_overview_metrics_rows, serving_account_names, serving_crm_totals, serving_crm_board_stages, serving_conversion_rows, serving_conversion_actions_mapping_state, serving_data_status, serving_account_inventory(_with_activity), serving_freshness). Compared against BigQuery marts (mart_paid_media_overview, mart_conversion_summary, mart_crm_funnel_snapshot).
Netlify (Next.js, runtime nodejs) in us-east-2; Supabase Postgres us-east-1 (pooler, IPv4); Cloud Run job in us-east1 for the k6 floor runner; gcloud/Cloud Logging for execution logs.
verify-serving-parity (owner) 180/180; verify-scoped-parity (scoped editor) 39/40; verify-tenant-isolation (fixture) 142/142; verify-crm-funnel 20/20; verify-overview-metrics 49/49; k6 floor runs (us-east1, n=40); BigQuery job counting in-render; fallback exercise via EXECUTE revoke incl. PUBLIC; build:check (ESLint/rules-of-hooks) + local render (not boot).
Code
export async function readVia<T>(page, read, fromServing, fromBigQuery): Promise<T> { if (!(await useServing(page))) return fromBigQuery(); try { return await fromServing(); } catch (e) { console.error([serving] FALLBACK TO BIGQUERY page=${page} read=${read}: ${e.message}, + serving is degraded for this page and BigQuery is answering instead. This is not normal.); return fromBigQuery(); }
}export type ServingPage = | "overview-metrics" | "linked-accounts" | "data-integrity" | "conversions" | "classifications" | "clients-console" | "geo" | "portal" | "wizard";
const ENABLED_PAGES = new Set((process.env.SERVING_PAGES ?? "").split(",").map(p=>p.trim()).filter(Boolean));
export function servingEnabledFor(page: ServingPage): boolean { return ENABLED_PAGES.has(page); }const PAGES_WITH_FALLBACK = new Set<ServingPage>(["overview-metrics"]); // module load: if SERVING_PAGES lists a page not here -> throw // useServing(page): if enabled but a bare gate is reached -> throw
A multi-agent workflow: the human owns architecture and go/no-go; this assistant (Cowork) reviews CC's reports, pressure-tests designs, catches latent failure modes (silent narrowing, one-line-away fallbacks, controls that can't fail), and writes the exact prompts/mandates; a separate Claude Code agent ('CC') implements, deploys, and measures in its own sessions, reporting results with evidence. When CC exhausted its context, a full cold-start onboarding prompt (Rule 0 = 100% adherence to the plan; 9 standing rules incl. Spanish-with-human/English-in-code, measure-don't-reason, no-silent-wrong, verify-from-the-losing-side) re-established the contract.
A repeatable per-page migration procedure (gcloud auth → diff → build:check + render → push → /api/v1/me=401 gate → SERVING_PAGES flip → five deliverables) run by the AI agent with the human approving diffs and the flip.
One human + one Claude Code agent migrated 5 production serving surfaces with per-page parity, isolation and fallback proofs; quantified time/headcount savings were not stated in-chat.
Measured Results
Migrated five production serving surfaces (overview-metrics, linked-accounts, data-integrity, conversions, crm) from BigQuery to Postgres behind a per-page flag with a loud fallback, each passing exact parity (owner + scoped), zero-BigQuery-jobs renders, real round-trip timings and an end-to-end fallback exercise. Conversions and CRM converged well under target (420/684 and 389/659 ms); Overview Metrics dropped from a BigQuery-dominated path to 793 ms with residual work handed to Phase 2B. Multiple silent-failure modes were caught before they reached a client.
| Metric | Value | Before | Source |
|---|---|---|---|
| Infra floor (TLS+edge+function+render, no session/queries), co-located us-east1 Reported by CC | p50 75 ms / p95 109 ms, 180/180 checks | n/a | k6 floor.js from us-east1 |
| Page floor, Users & Roles (lightest authenticated page, zero BigQuery) Reported by CC | p50 297 ms / p95 600 ms, n=40 | n/a | run-page-floor.js (us-east1) |
| Overview Metrics page floor after Postgres migration Attributed as data path (render only 80 ms); residual is ~13 round-trips + moving 4,205 rows; a 2B target | p50 793 ms / p95 1466 ms (p99 4400 ms cold start), n=40 | n/a | run-page-floor.js (us-east1) |
| Conversions page, before → after migration Reported by CC; page floor 334/1633 same day | 911/1410 ms → 420/684 ms (p50/p95) | 911/1410 ms | us-east1 runner |
| CRM Pipeline page, before → after migration Reported by CC | 892/1242 ms → 389/659 ms (p50/p95) | 892/1242 ms | us-east1 runner |
| Linked Accounts / Data Integrity floors Reported by CC | Linked Accounts 442/841 ms; Data Integrity 401/1271 ms (p50/p95) | n/a | us-east1 runner |
| In-function round-trip (RLS RPCs, IAD) Real RPCs faster than the synthetic query | auth.getUser 20.6 ms; serving_crm_totals 22.1 ms; serving_account_names 22.8 ms; serving_freshness 23.4 ms; serving_overview_metrics_rows 30.1 ms p50 / 139.9 ms p95 | §11C synthetic join+limit 36-45 ms | from inside the function |
| BigQuery jobs in render (healthy page) Zero-jobs only counts alongside 200 + parity green | 0 jobs | 4 jobs (flag absent) / 3 jobs | counted during a real render, not inferred |
| Parity, owner + scoped + isolation The single scoped failure is the real-data widening case, blocked because only one client (Swap Commerce) has accounts | verify-serving-parity 180/180; verify-scoped-parity 39/40; verify-tenant-isolation 142/142 | n/a | verification scripts |
| Float parity bound (measured) Replaced an arbitrary tolerance with measurement-derived bounds | worst 15 ULP (3.1e-15) over ~20k terms; bound 4.4e-12; sameNumber 1 ULP | flat 1e-9 (replaced) | numeric.cjs over 104 cells (79 bit-identical) |
Every figure above was recorded during the work itself. Where no number was measured, none is claimed.