← Back to journal
May 28, 2026·3 min read·Building TarmacLabs

The build that almost broke

Hours after AutoBrief went live, my next push to tarmaclabs.org failed in production. The change had nothing to do with the file that crashed. Here's the trap that caught me.

By David Sawires
Share

Tonight I shipped an RSS feed to tarmaclabs.org/rss.xml. Forty lines of TypeScript, a single new file, a new sitemap entry. The diff didn't touch authentication or the database or anything that could plausibly affect a different page.

The deploy failed. The page that crashed was /admin/leads— a route that hasn't been touched in two days.

The error

Vercel's build log said: Invalid supabaseUrl: Must be a valid HTTP or HTTPS URL. It came from page-data collection on /admin/leads. The chain led back to lib/supabase.ts:

import { createClient } from "@supabase/supabase-js";

const url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

export const supabase = createClient(url, key);

Three lines. Two non-null assertions. One call to createClient() at module load.

Why it had been working

The trap: Vercel had been restoring the previous build cache on every deploy. The module-evaluation result was cached. The eager createClient(URL, KEY)never re-ran. The env-var assertion was a Schrödinger's bug — passing only because nothing was forcing the box to open.

Tonight's RSS feed change happened to invalidate something the cache was holding. Clean rebuild. The eager evaluation fired. The env vars on the Vercel free tier scope I'd been using weren't fully propagating to that build env. createClient threw. Whole deploy red.

A bug that only fires on a clean rebuild is the worst kind of bug: invisible during normal development, fatal during a fresh deploy.

The fix

Lazy initialization. lib/supabase.ts now exports a Proxy that resolves the real client on first property access. The createClient call only runs when something actually touches supabase.from(). Module-load is harmless.

let _client: SupabaseClient | null = null;
function getClient() {
  if (_client) return _client;
  const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
  const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
  if (!url || !key) throw new Error("Env vars missing");
  _client = createClient(url, key);
  return _client;
}

export const supabase: SupabaseClient = new Proxy({} as SupabaseClient, {
  get(_target, prop) {
    const client = getClient();
    const value = (client as any)[prop];
    return typeof value === "function" ? value.bind(client) : value;
  },
});

Every callsite that used supabase.from("…")works unchanged. The proxy just defers the failure from build-time to first-call. If env vars are still missing at runtime, the error message says exactly what to set and where. The proxy doesn't hide problems — it relocates them to the moment they can actually be diagnosed.

The lesson

Anything that touches credentials at module-load is a deploy landmine waiting to be tripped. The right shape for third-party SDK init is always lazy: getter pattern, proxy pattern, or just inline-construct it inside each handler. Top-level new SDK(process.env.X!) looks tidy and is a time bomb.

I added this to the decisions log. Future me, future Claude — every new SDK wrapper in this codebase gets the proxy treatment by default.

And the RSS feed?

It's live now. Twenty-one posts. Auto-discovered by every reasonable feed reader. The little orange icon in the journal page header is the entry point. The cache-masked bug got found AND fixed before the day ended. Net win.

Share