TLDR: One auth check isn't defense-in-depth. Mirror it at the API handler, the middleware, AND the route layout — and make every gate fail closed when the env var is missing.

The Setup

I was doing a night audit on a cancer education business's patient-matching app (a Next.js app that matches patients with cancer treatment facilities) when I found something that made my stomach drop.

The /admin routes had auth protection — requireAuth() in the API handlers, a check in the Supabase (my database platform) middleware. Looked fine at a glance.

But the route layout itself? src/app/admin/layout.tsx?

Nothing.

What Actually Broke

Here's the part that made it worse.

Both requireAuth() and the middleware keyed off the ADMIN_EMAIL env var — the address of the authorized admin. But if that var was missing or empty, neither check actually ran.

They silently passed.

So: two failure modes stacked on top of each other. First, the layout had no gate at all — any signed-in user could hit those routes if the middleware ever got skipped (misconfigured rewrites, direct RSC access, you name it). Second, if the deploy was misconfigured and ADMIN_EMAIL was blank, the existing checks just… waved everyone through.

Fail-open. The worst kind of bug, because it looks like it's working. The check exists. The code is there. The protection just doesn't apply.

The Fix That Stuck

Three gates, all fail-closed:

  • src/lib/auth.ts::requireAuth() — throws if ADMIN_EMAIL env var is missing or the session email doesn't match; no bypass on empty config
  • src/lib/supabase/middleware.ts::updateSession() — redirects to / if the check fails or if the env var is missing
  • src/app/admin/layout.tsx — server component that re-runs the check at render time; missing env var = redirect, every single time

Now if one layer has a bug or a misconfigured deploy, the other two catch it. That's the whole point.

A few weeks later, a nearly identical pattern surfaced at the DB layer on an ecommerce client's shared Supabase project. A developer I work with noticed the RLS (Row Level Security, Postgres's built-in table-level permission system) policies were to authenticated using (true) — which meant any Google account could authenticate and read/write the tables directly through PostgREST (the auto-generated REST API Supabase exposes over your tables), completely bypassing the app-layer gate.

56,193 Shopify orders. Wide open.

Same lesson, different layer.

Why This Matters to Me

The thing about fail-open bugs is they look secure.

Someone — probably you, probably me — wrote that check with good intentions. It was real code doing real work. It just didn't hold when the environment was slightly wrong.

The rule I'm applying now: every admin gate lives in at least two places, preferably three. And every gate fails closed when its config is missing.

Missing env var should mean deny all, not skip the check.

It's three extra if (!adminEmail) redirect('/') lines. Ship them.