A shipped cleanup that stops carrying placeholder NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY values in frontend/.env.production, which existed only to stop the supabase-js constructor throwing during the static-export build even though entra mode never calls the client. Converts frontend/src/lib/supabase.ts from a module-level export to a lazy getSupabaseClient() factory that throws a clear error (mentioning /api/config and the active authProvider) when the env vars are unset, with lib/auth.ts and lib/supabase-server.ts following the same pattern and every call site gated to the supabase branch. The backend's own supabase-js PostgREST usage is unchanged; this is the small concrete step toward issue 019's broader isolation and unblocks issue 032's removal of .env.production. When a non-supabase build fails on missing supabase env vars, or how getSupabaseClient() lazy construction works.
Issue 031 — Supabase Placeholder Removal
Status: shipped. frontend/src/lib/supabase.ts exports getSupabaseClient() lazily.
lib/supabase-server.tsandlib/auth.tsupdated to throw clear errors / return null instead of asserting on placeholder values. Login page calls getSupabaseClient() only inside the supabase branch.AuthContextcalls it only inside the supabase arm of the provider switch.
Goal
Stop carrying NEXT_PUBLIC_SUPABASE_URL and
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY placeholder values in
frontend/.env.production. Make the supabase client construct lazily
and only when supabase mode is actually active, so non-supabase
deployments need none of these env vars to build or run.
Context
Today frontend/src/lib/supabase.ts constructs the supabase client at module load:
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || (isLocalAuth ? "http://localhost:8000" : "");
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY || (isLocalAuth ? "local-dev-key" : "");
export const supabase = createClient(supabaseUrl, supabaseAnonKey);
frontend/src/lib/auth.ts uses non-null assertions on the same env vars, which would explode at module load if they were genuinely missing:
return createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!,
);
In Entra mode the client is never called. The only reason these
values exist in frontend/.env.production is to keep the supabase-js
constructor from throwing during the static-export build:
# frontend/.env.production
NEXT_PUBLIC_SUPABASE_URL=https://placeholder.invalid
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=placeholder
This is an "appease the constructor" workaround. It surfaces in several bad ways:
frontend/.env.productioncarries values that mean nothing — reviewers have to remember "those aren't real."- A future bug that accidentally calls the supabase client in entra
mode would fire HTTP requests at
placeholder.invalidand fail in a confusing way instead of failing fast. - It blocks the issue 032 cleanup: even after issue 030 lands and
every
NEXT_PUBLIC_ENTRA_*is gone, the supabase placeholders still force.env.productionto exist.
What to build
frontend/src/lib/supabase.ts
Convert from a module-level export to a lazy factory:
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
let _client: SupabaseClient | null = null;
export function getSupabaseClient(): SupabaseClient {
if (_client) return _client;
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
const key = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;
if (!url || !key) {
throw new Error(
"Supabase client requested but NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY are not set. " +
"This deployment is in non-supabase mode — check authProvider in /api/config.",
);
}
_client = createClient(url, key);
return _client;
}
Update every existing import { supabase } from "@/lib/supabase" to
call getSupabaseClient() at the call site. Search:
grep -rn 'from "@/lib/supabase"' frontend/src
For each call site:
- If the call is inside an
if (authProvider === "supabase")branch (or equivalent), call getSupabaseClient() directly. - If the call is in a code path that runs in all modes (e.g. an
effect that subscribes to auth-state changes), gate it on
useConfig().authProvider === "supabase"(post-issue-030) before invoking the helper.
frontend/src/lib/auth.ts
Same pattern. Replace the non-null-asserted module-level call with a function:
export function getServerSupabase(): SupabaseClient {
return getSupabaseClient();
}
Or delete auth.ts if it's only re-exporting the module-level client
— it doesn't add anything once the export is a function.
frontend/src/lib/supabase-server.ts
Mirrors the same change. Convert any module-level createClient
calls to lazy factory functions.
Frontend env-var deletes
After this issue:
- Delete
NEXT_PUBLIC_SUPABASE_URLandNEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEYfromfrontend/.env.production(issue 032 removes the file entirely). - Keep them in frontend/.env.local.example under a clearly-marked "supabase mode only" section.
Backend (no changes)
The backend's @supabase/supabase-js usage (createServerSupabase)
is independent — it's a server-side PostgREST client and is gated
appropriately on AUTH_PROVIDER. No changes here.
Acceptance criteria
- [ ] frontend/src/lib/supabase.ts exports a getSupabaseClient()
function, not a module-level
supabaseconstant. - [ ] No call site imports
supabaseas a module-level value; every consumer calls getSupabaseClient(). - [ ] In a clean checkout with neither
NEXT_PUBLIC_SUPABASE_URLnorNEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEYset,npm run build --prefix frontendsucceeds. - [ ] In a build of the entra-mode bundle (no supabase env vars set), end-to-end usage of the deployed app does not throw.
- [ ] In supabase mode, all upstream supabase auth flows still work (regression check on the upstream-shaped behaviour).
- [ ] Calling getSupabaseClient() when env vars are unset throws a
clear error mentioning
/api/configand the currentauthProvider.
Out of scope
- Removing
@supabase/supabase-jsfrompackage.json. Long-term goal but a separate issue (the backend still uses it as a PostgREST client). - Changing the supabase auth flow itself.
- The bigger frontend hosted-dependency-isolation work — see
019-frontend-hosted-dependency-isolation.md.
Related
- Issue 019 — frontend hosted-dependency isolation. This issue is the small concrete step; 019 is the broader cleanup.
- Issue 030 — runtime config endpoint. Independent but expected to land around the same time.
- Issue 032 — retire
frontend/.env.production. Depends on this issue plus 030. - docs/migration/05-config-extraction.md — explains why the placeholder values are tier-C-adjacent: they aren't secrets, but their presence in the repo is a smell.