Skip to content

Environment Secrets and Security Boundaries

Goal

Understand the client/server environment boundary in LenserFight, correctly place secrets in the right .env file, and verify that no server-only secrets are leaking into the browser bundle.


Prerequisites


Expected Result

  • SUPABASE_SERVICE_ROLE_KEY and SUPABASE_SERVICE_ROLE_KEY are never included in any client-side bundle
  • VITE_ prefixed variables are intentionally public and known to you
  • You can audit your own setup using the built-in tools

The Two-Environment Model

LenserFight runs across two distinct runtime environments with different trust levels:

EnvironmentExamplesWho can read it
Server / CLI / WorkerNode.js, Cloudflare Workers, Supabase Edge Functions, the lf CLIBackend processes only
Browser (client)apps/web, apps/arena, apps/auth — compiled by ViteAnyone with DevTools

The fundamental rule: anything in the browser bundle is public. API keys, service role tokens, and database passwords must never reach the client.


How Vite Enforces the Boundary

Vite (used by all browser apps in this monorepo) only exposes environment variables to the browser if they are explicitly prefixed with VITE_.

Any variable without the VITE_ prefix is stripped from the client build. This is not optional — Vite enforces it at build time.

# .env.local in apps/web/

VITE_SUPABASE_URL=http://127.0.0.1:54321       # ✅ Browser-safe — public URL
VITE_SUPABASE_ANON_KEY=eyJhbGci...             # ✅ Browser-safe — anon key is designed to be public
VITE_APP_ENV=development                        # ✅ Browser-safe — non-sensitive

SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...          # ✅ Server-only — Vite strips this
SUPABASE_URL=http://127.0.0.1:54321            # ✅ Server-only — stripped (no VITE_ prefix)

The Anon Key is Public by Design

The Supabase anon key is not a secret. It is a JWT with a fixed role: "anon" claim that PostgREST uses to identify unauthenticated requests. Its security model is Row-Level Security, not key secrecy.

It is safe to:

  • Commit VITE_SUPABASE_ANON_KEY to your .env.example
  • Expose it in the browser bundle
  • Log it in development

It is not safe to:

  • Use the anon key on the server to bypass RLS (use the service role key for that, server-side only)

Variable Placement Reference

Use this table to decide which file a variable belongs in:

VariableServer-only?Lives in
SUPABASE_SERVICE_ROLE_KEYYesRoot .env.local only
SUPABASE_URLYes (server builds)Root .env.local
VITE_SUPABASE_URLNo — browser safeapps/web/.env.local
VITE_SUPABASE_ANON_KEYNo — browser safeapps/web/.env.local
LENSERFIGHT_API_KEYYesRoot .env.local only
CHAINABIT_PARTNER_API_KEYYesRoot .env.local only
LENSERFIGHT_KEYS_PASSPHRASEYesRoot .env.local only
LF_GATEWAY_BINDYes (gateway daemon)Root .env.local only
GEMINI_API_KEYYesRoot .env.local only
VITE_CAPTCHA_SITE_KEYNo — browser safeapps/web/.env.local
VITE_POSTHOG_PROJECT_TOKENNo — browser safeapps/web/.env.local

Steps

1. Map your variables to the correct files

For a new variable, ask three questions:

  1. Does this variable contain a credential or token that grants server access? → Server-only. Put it in root .env.local.
  2. Is this variable needed at build time in the browser app? → Prefix with VITE_ and put it in apps/web/.env.local.
  3. Is this variable needed only by the CLI or worker? → Root .env.local or the app's own .env.local.

2. Verify the build does not leak server-only variables

After any .env change, inspect the built bundle:

bash
# Build the web app
pnpm nx run web:build

# Search the output for known secret values
grep -r "your-service-role-key-prefix" dist/apps/web/

If grep finds a match in dist/apps/web/, the secret was included in the client bundle. This is a build configuration error.

More comprehensive audit:

bash
# Check for any Supabase service role JWT structure in the bundle
grep -r "service_role" dist/apps/web/ 2>/dev/null | grep -v ".map"

The service role JWT contains the literal string "role":"service_role" — if that appears in any dist/apps/web/*.js file, a secret leaked.


3. Audit your .env.example files

The .env.example files are committed to the repository. They must contain only placeholder values — never real credentials.

Check the examples:

bash
# Ensure no real keys are in example files
cat .env.example | grep -v "^#" | grep "KEY\|SECRET\|PASSWORD\|TOKEN"

Each sensitive-looking value should be a placeholder like your-service-role-key-here or be left empty.


4. Use lf security for a runtime audit

The CLI includes a security audit command:

bash
lf security audit

This checks:

  • Whether the CLI's active config uses the service role key where the anon key should be
  • Whether any .env files in the current project expose known high-risk patterns
  • Key store encryption status

5. Supabase Edge Functions

Edge Functions are server-side (run on Deno in Supabase's infrastructure). They can safely read secrets from Supabase's encrypted secret store:

bash
# Set a secret for an Edge Function
pnpm supabase secrets set MY_API_KEY=sk-real-key-here

# List secrets (values masked)
pnpm supabase secrets list

Do not hardcode API keys in Edge Function source files. Source files are committed to the repository — only the secrets store is encrypted and access-controlled.

For local development, Edge Functions read from supabase/functions/.env (gitignored):

bash
# supabase/functions/.env
MY_API_KEY=sk-dev-key-for-local-only

Common Issues

Issue: A new environment variable is undefined in the web app

Cause: The variable was added to root .env.local without a VITE_ prefix.

Fix: Add a VITE_-prefixed version to apps/web/.env.local:

bash
# apps/web/.env.local
VITE_MY_NEW_VAR=value

Then access it in code as import.meta.env.VITE_MY_NEW_VAR.


Issue: A secret appears in the browser bundle

Cause: The variable was accidentally given a VITE_ prefix, or was imported into a shared module used by both server and client code.

Fix:

  1. Remove the VITE_ prefix from the variable in apps/web/.env.local.
  2. Audit any import.meta.env.VITE_* references that reference secrets.
  3. Move secret access to server-only code paths (Edge Functions, Worker, Node.js scripts).

Issue: SUPABASE_SERVICE_ROLE_KEY used client-side to bypass RLS

Cause: A developer used the service role key in a React component or frontend module to work around a missing RLS policy.

Why this is critical: Using the service role key client-side exposes it to any user who opens DevTools. It grants full database access to anyone who finds it.

Fix:

  1. Remove the service role key from all client-side code immediately.
  2. Write a proper RLS policy for the data access pattern that was being bypassed.
  3. If the operation truly requires elevated permissions, move it to a Supabase Edge Function with the service role key set as a Supabase secret.

Issue: CLI fails with missing required env var

Cause: A required variable for the CLI is not set in the environment the CLI is invoked from.

Fix:

bash
# Check what the CLI sees
lf env

# Load your .env.local into the shell session
set -a && source .env.local && set +a
lf <command>

Security Checklist

Before opening a PR that changes environment configuration:

  • [ ] No real credentials in .env.example files
  • [ ] All new VITE_ variables are genuinely non-sensitive
  • [ ] SUPABASE_SERVICE_ROLE_KEY is not in any apps/web/ env file
  • [ ] Any secret used in an Edge Function is set via pnpm supabase secrets set, not hardcoded in source
  • [ ] dist/apps/web/ bundle does not contain any credential strings
  • [ ] lf security audit passes

Next Steps