Chapter 25. 2026-07-16: The Silent Fallback That Took Production Down

On 2026-07-16 the Suede Agent Studio at agents.suedeai.ai was backed by a non-durable SQLite file inside a serverless function. Vercel’s Production env vars for the datastore, DB_DRIVER and the SUPABASE_* group, were pulling as empty strings; the repository layer read the empty driver value, raised nothing, and took its fallback path, holding production state for a product that exists to keep what members build.

The multi-lane audit that surfaced it came back with real P0 findings: a false save/recovery conflict that auto-persisted a draft on a mere page view, a disabled Run button, live /api/v2/* endpoints 503ing, and mobile and WCAG failures stacked on top. A list like that invites four separate investigations. The visual and UX items turned out to be what they looked like, and they got their own fix. The rest sat on a single structural fault that was not in the application code at all.

Why SQLite in a serverless function cannot hold production state

SQLite in a serverless function cannot hold production state, because the database is a file on the function instance’s local disk. Instances get recycled without notice, a fresh instance starts with nothing, and two concurrent instances each hold a private copy of the truth. A write returns success, and the success means “written to a disk that may not exist in an hour.” No exception fires anywhere in that arrangement. The endpoints answer, the canvas renders, and the store underneath is scratch paper.

Agent Studio is the builder. Members assemble node-graph flows on a canvas and launch them as x402 pay-per-call endpoints that settle USDC on Base through the Coinbase CDP facilitator. The product’s job is to hold a flow today and run it tomorrow, with money attached, and that is the promise a file on a recycled disk cannot make.

My notes do not map each P0 finding to the fallback one for one, and the mapping matters less than the mechanism. A store whose contents differ per instance and vanish on recycle is a machine for manufacturing this class of bug: state that a member saved and cannot recover, state that recovers when nobody saved it, endpoints that answer from an instance holding tables and 503 from an instance holding none. From the outside, that presents as several intermittent bugs in unrelated features. From the inside, it is one line of config resolving to the wrong branch. The worst property of the failure is that it degrades the product into something that mostly works, and “mostly works” generates bug reports instead of alarms.

The repo’s own split made the trap hard to see from a laptop. Dev runs sqlite by design. Prod is supposed to run Supabase, selected by DB_DRIVER=supabase, and there is no local .env for prod, so nothing on my machine can testify about what production is doing. The one instrument that reaches the real answer is curl against production itself. That rule sits in my operating notes now, and this is the day that wrote it.

Why an empty env var passes the check a missing one fails

An empty string is present. It passes the existence check and fails the meaning check, and a config reader with a default bolted on treats "" as a request for the default. A missing variable fails presence checks; code that requires it tends to crash early and name it in the stack trace. The distinction carried the whole failure. DB_DRIVER equal to "" is not equal to "supabase", so the driver switch chose sqlite, the same branch it chooses in dev, where that choice is correct. The behavior that makes local development pleasant masked a production outage, and masked it without one log line of complaint. My notes do not record why the vars pulled empty; the record starts at what the emptiness did.

The audit found a second layer under the empty vars, and the second layer was worse. Agent Studio’s Supabase runtime had never been implemented. Phase 9 of the master plan assumed DB_DRIVER=supabase in prod; the assumption lived in the plan and never became code. Populating the env vars alone would not have saved anything, because production had no Supabase path to take.

The June “verified live” entry, and why it proved nothing

My project notes from 2026-06-11 state that migrations were applied to prod that evening and verified live: the gateway llm endpoint returning 200 with metering, topup issuing 402 challenges with a real payTo. The same file, five weeks later, states the Supabase runtime was never implemented. Both entries sit there today.

Two readings survive contact with the evidence. Either the June verification exercised endpoints that succeeded against the SQLite fallback, since a 200 and a 402 challenge come back byte-identical no matter which database produced them, or the production env regressed somewhere between 2026-06-11 and 2026-07-16. The notes do not say which, and I decline to resolve it by picking the reading that flatters the June entry. The same file carries a second internal wrinkle: one line marks the relay migration SQL in docs/migrations/phase-8-relay.sql PENDING on prod Supabase, while a later line calls the PENDING block historical. I flag both rather than harmonize them, because a note that smooths over its own contradictions lies with confidence.

The June entry holds the sharpest lesson in the incident. On 2026-06-11 the repo also showed all 10 master-plan phases complete, 355 of 355 vitest passing, and a clean build. Green tests prove the code agrees with the tests. A green endpoint check proves the route works. Neither one identifies which backend served the request. “Verified live” was true and useless at the same time: the check measured HTTP behavior while the question was durability. A durability claim needs a durability test. Write a record, redeploy, read it back. The redeploy is the step that does the work, because it recycles the function instance and takes the scratch paper with it. Ephemeral SQLite passes a write-then-read check within one instance’s lifetime, and it passes with a straight face.

The PRs that landed on 2026-07-16

Two PRs carried the repair on 2026-07-16. PR #71 took the visual and UX findings. PR #72 took the database: proper Supabase config restored, RLS hardened, the service-role key removed from Vercel, atomic RPCs added. Agent Studio’s tables landed in the same shared Supabase project that serves the rest of the estate. That placement is deliberate and temporary, my call, and I want it on the record as a choice so no future audit reads it as an accident. An end-to-end canary passed: create, bind, version, run. PR #73 closed the day with a small fix to the persistent top nav. PR #69 landed the same day too, converting 12 Business Skills templates from decorative shells into action-capable workflows, all verified 200 end-to-end on the live site.

The terminal handoff for the day states “no release blocker remains.” My notes carry a caveat beneath it that I wrote on purpose: given how much churn happened in one day, the work deserves an independent spot-check before anything release-critical relies on it. Both lines are honest, and they belong together. A day that merges four PRs, rewires the production datastore, and tightens security posture produces claims that should be re-verified by someone who was not sprinting through the day.

Three empty-env incidents in five weeks

Agent Studio was the third empty-env incident in this estate inside five weeks.

Date Surface Empty in production What the code did instead of failing
2026-06-11 strumly.suedeai.ai STRUMLY_PAYMENT_MODE Fell into stub payment mode and accepted any forged X-Payment header as paid, proven live
2026-07-15 print.suedeai.ai/app VITE_SUPABASE_URL, VITE_SUPABASE_ANON_KEY Built and served a degraded client with sign-in fully broken; the build stayed green
2026-07-16 agents.suedeai.ai DB_DRIVER, SUPABASE_* Fell back to a non-durable SQLite repo in serverless

The three incidents share one shape across three unrelated stacks. In each case a required dependency was absent, and in each case the code held an opinion about what to do instead of an error to raise. Strumly trusted forged payment proofs. The voice web app rendered a sign-in that could not sign anyone in, and Agent Studio kept production state on a disk with the lifespan of a function instance.

Strumly also carries the repair I treat as the standard now. A guard in lib/api/payment.ts makes production throw at boot unless STRUMLY_PAYMENT_MODE is exactly “live” or “stub”. An ambiguous value refuses to boot. That guard converts a live revenue hole into a failed deploy, and a failed deploy costs minutes and embarrasses no one but me.

The same Agent Studio repo had already taught the fail-open lesson on the money path. On 2026-06-11, Phase 9 shipped the settlement toggle opt-in with a default of false. Production had no settlement_live column, so each agent mapped to dry-run, and dry-run skips the x402 402 challenge. Priced agents were free to call while settlement was live. An orchestrator spot-curl caught it, and hotfix commit 12d841b inverted the default: missing or NULL means LIVE, an explicit owner toggle-off is the one way to disable settlement, and env X402_SKIP_SETTLEMENT is the sole kill-switch. The datastore fallback is the same species. Code met an absent value and chose the permissive branch. The settlement fix chose the right posture: when a required signal is missing, fail toward the behavior that protects the promise. On a money path that means charge. On a datastore path that means refuse to boot.

Fail the boot when required config is empty

A fallback that hides a missing dependency is a bug with a delay on it. The delay is the expensive part. The deploy goes green, the demo works, the first sessions look fine because a warm instance keeps its file for a while, and the failure arrives later wearing a disguise: a save/recovery conflict, a 503, a Run button that will not enable. By then the incident costs a multi-lane audit instead of a failed boot.

Four rules came out of this day.

Required config gets validated at boot, and "" counts as missing. The check belongs where the process starts, not where the value gets used, and the error should name the variable and show the value it received. The shape I want in any repo with a driver switch:

// Boot guard. JSON.stringify distinguishes "" from undefined in the message.
const driver = process.env.DB_DRIVER;
if (process.env.VERCEL_ENV === "production" && driver !== "supabase") {
  throw new Error(
    `DB_DRIVER must be "supabase" in production, got ${JSON.stringify(driver)}`
  );
}

Dev fallbacks require an explicit opt-in. sqlite selected by DB_DRIVER=sqlite is a feature. sqlite reached by falling through a condition that nothing satisfied is a trap, and the trap arms itself the moment an env var reads as "".

The datastore gets verified from production, not from config intent. For Agent Studio that means curl against agents.suedeai.ai, because dev is sqlite, prod claims Supabase, and no local file can testify about the difference.

A datastore change earns a durability test. Write, redeploy, read back. A 200 is a statement about the route. Survival across a redeploy is a statement about the database.

I keep the June note in the file above the July one, both uncorrected. The pair says more than any rule I could append under them: a verification is a claim about the check you ran, not about the system you ran it against. In June I asked production whether it could answer HTTP, and it could. The question that mattered was where production kept its memory, and the honest answers to that one are a boot-time throw or a record that outlives a redeploy.