Environment variables
Copy .env.example to .env and fill in the values before running the dev server or deploying. All variables are read at runtime by the Netlify function — none are baked into the static build. On a provisioned site the provisioner writes the whole set into Netlify’s environment; you only need a local .env for pnpm dev.
This page is the setup walkthrough. The per-variable table — every name, who reads it, and what happens when it is unset — lives in one place, the Environment variable reference. Nothing below repeats it.
GitHub
Section titled “GitHub”GITHUB_TOKEN, GITHUB_OWNER, GITHUB_REPO, and optionally GITHUB_BRANCH (default main). For a standalone site the token is a fine-grained PAT with read/write access to the client repo’s Contents plus the mandatory read-only Metadata permission, scoped to that single repository. Provisioned sites mint GitHub App installation tokens through the platform broker instead and hold no PAT.
Session secret
Section titled “Session secret”SESSION_SECRET signs the session cookie in every auth mode and must be at least 32 characters. Generate one with the command .env.example shows:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"(openssl rand -base64 32 produces an equally good value.)
Auth provider
Section titled “Auth provider”AUTH_PROVIDER is password (the default when omitted) or supabase.
Password mode
Section titled “Password mode”ADMIN_PASSWORD, EDITOR_PASSWORD, and one VIEWER_<NAME>_PASSWORD (plus optional VIEWER_<NAME>_COLOR) per gated viewer audience. All *_PASSWORD values are bcrypt hashes, not plaintext:
node -e "console.log(require('bcryptjs').hashSync('your-password', 10))"Critical — escape every $ in the hash with a backslash. Vite runs dotenv-expand, which interprets $name as a variable reference and silently strips it, corrupting the hash. Quoting the value does NOT prevent this; only \$ does.
# bcryptjs gives you:$2b$10$n1JHs0z5qYC.ISZG...
# Write in .env as:ADMIN_PASSWORD=\$2b\$10\$n1JHs0z5qYC.ISZG...Second gotcha: no leading whitespace before the key name. dotenv silently skips indented lines, so every sign-in attempt will fail with “Invalid password” — with no error in the logs.
Supabase mode
Section titled “Supabase mode”SUPABASE_URL and SUPABASE_ANON_KEY, plus one of the two privileged-access shapes below. SUPABASE_ACCOUNT_TOKEN and SUPABASE_PROJECT_REF are only for running migrations with the CLI (pnpm db:push), never at runtime.
Platform mode vs standalone
Section titled “Platform mode vs standalone”A Supabase-auth site needs either:
- Platform mode —
PLATFORM_API_URL,PLATFORM_API_KEY,PORTAL_SITE_ID. The default for every provisioned site: privileged operations (auth admin, R2 asset and document storage, GitHub App writes, chat, deploy status) are brokered through the admin app, so the site holds no provider credentials at all. The provisioner sets these; do not hand-fill them. - Standalone —
SUPABASE_SERVICE_ROLE_KEYon the site itself (server-only), and if the site usesr2Assets()/r2Documents(), its ownR2_ACCOUNT_ID,R2_ACCESS_KEY_ID,R2_SECRET_ACCESS_KEY,R2_BUCKET,R2_PUBLIC_BASE(andR2_DOCS_BUCKETfor documents over 2.5 MB), plusPORTAL_SITE_IDbecause object keys are tenant-prefixed in both modes.
Never both. Video embeds, small looping video uploads (5 MB or less), and documents up to 2.5 MB need no bucket in either mode — see Using video and Section types.
Deploy
Section titled “Deploy”SITE is the canonical origin of the site, e.g. https://acme.drawn.guide, and must be pinned to the custom domain — not the *.netlify.app subdomain. It is baked into invite and password-reset emails; Netlify’s built-in URL can resolve to the Netlify subdomain instead, so an explicit SITE is required in Supabase mode. The provisioner sets it, along with NETLIFY_WEBHOOK_SECRET (which lets the site verify Netlify’s deploy webhooks for build status). For sites built before either was introduced, set them by hand in the Netlify dashboard and trigger a redeploy.