---
title: "Secrets and Environment Variables: Best Practices"
slug: "secrets-and-env"
category: "ops"
tags: ["secrets", "security", "env", "ops", "supabase"]
status: "stable"
last_updated: 2026-06-16
summary: "Keep secrets out of client bundles and out of git; store them in platform secret stores and rotate any that leak."
related: ["[[tooling/github-secrets]]", "[[backend/supabase-rls]]", "[[ops/vercel-env-vars]]", "[[coding/python-security]]", "[[ops/pre-launch-checklist]]", "[[backend/webhooks]]", "[[backend/supabase]]"]
---

## Overview

A secret is any value that grants access if leaked; keep it on the server, out of git, and inside a platform secret store. Public config (API base URLs, feature flags, publishable keys) can ship to the browser. Everything that authenticates or authorizes (database passwords, service keys, signing secrets, third-party API tokens) must not. Misclassifying one value is the difference between a config bug and a full account takeover.

## Treat any client-prefixed variable as public

Build tools inline browser-facing env vars into the JavaScript bundle at build time. In Next.js, any variable prefixed `NEXT_PUBLIC_` is inlined into the bundle and visible to anyone who opens devtools ([Next.js docs](https://nextjs.org/docs/pages/guides/environment-variables)). In Vite, any variable prefixed `VITE_` is exposed on `import.meta.env` in client source after bundling ([Vite docs](https://vite.dev/guide/env-and-mode)). Both prefixes are a one-way door: once a value carries that prefix and ships, assume the public has read it.

Rule: NEVER place a secret behind `NEXT_PUBLIC_` or `VITE_`. Reserve those prefixes for values that are safe on a billboard. Never set Vite's `envPrefix` to an empty string; that exposes every variable.

## The catastrophe: a server key in the client bundle

The classic failure is shipping a Supabase `service_role` key behind `NEXT_PUBLIC_` or `VITE_`. That key bypasses Row Level Security and grants full read/write to every table. In the browser bundle it is a public admin password to your database. Use the `anon`/publishable key on the client, enforce access with [[backend/supabase-rls]], and keep `service_role` server-side only. The same logic applies to any backend secret: Stripe secret keys, signing secrets for [[backend/webhooks]], and SMTP credentials all stay on the server. See [[backend/supabase]] for the client-versus-server key split.

## Never commit .env

`.env` files hold real secret values and must never enter version control. Add `.env`, `.env.local`, and `.env.*.local` to `.gitignore` before the first commit. Ship a `.env.example` that lists every key with empty or placeholder values so new contributors know what to set:

```
# .env.example
DATABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
NEXT_PUBLIC_SUPABASE_URL=
```

Commit `.env.example`; never commit `.env`. For language-specific loading and validation patterns, see [[coding/python-security]].

## Scan the repo and CI for leaks

Run a secret scanner locally and in CI so a leaked key fails the build instead of reaching production. Gitleaks is a SAST tool for hardcoded secrets that runs as a pre-commit hook or as `gitleaks/gitleaks-action` in CI ([gitleaks](https://github.com/gitleaks/gitleaks)). TruffleHog classifies 800-plus secret types and verifies them with live API calls, so `--only-verified` cuts false positives to real, active credentials ([trufflehog](https://github.com/trufflesecurity/trufflehog)). Scan full git history, not just the working tree; a deleted secret still lives in past commits.

## Store secrets in the platform's secret store

Inject secrets at runtime from a managed store, not from files baked into an image. Use Vercel project environment variables, scoped per environment, for deployed apps; see [[ops/vercel-env-vars]]. Use Supabase secrets for Edge Functions. Use GitHub Actions secrets for CI, and prefer OIDC for keyless cloud auth so workflows exchange short-lived tokens instead of holding long-lived cloud keys; see [[tooling/github-secrets]]. The goal is fewer standing secrets and shorter-lived ones.

## Rotate on exposure and on a schedule

Assume any secret that touched git, a log, a Slack message, or a client bundle is burned. Rotate it immediately, then audit access logs for use during the exposure window. Removing the commit does not help; the value is already cloned and indexed. Beyond incidents, rotate long-lived credentials on a fixed schedule so a quiet leak has a bounded blast radius. Confirm rotation coverage in [[ops/pre-launch-checklist]].

## Related

- [[tooling/github-secrets]] - CI secret storage and OIDC keyless auth.
- [[backend/supabase-rls]] - why the service_role key bypasses access control.
- [[backend/supabase]] - anon versus service_role key split.
- [[ops/vercel-env-vars]] - per-environment secret scoping on Vercel.
- [[coding/python-security]] - loading and validating env config safely.
- [[backend/webhooks]] - signing secrets that must stay server-side.
- [[ops/pre-launch-checklist]] - rotation and leak checks before launch.
