Skip to content

Environment variables

Copy .env.example to .env and fill in the values before running the dev server or deploying. All variables listed here are read at runtime by the Netlify function — none are baked into the static build.

VariableRequiredDescription
GITHUB_TOKENYesFine-grained personal access token with read/write access to the client repo’s Contents, plus the mandatory read-only Metadata permission. Scoped to the single client repository.
GITHUB_OWNERYesGitHub organization or username that owns the client repo.
GITHUB_REPOYesRepository name (without the owner prefix).
VariableRequiredDescription
SESSION_SECRETYesRandom string used to sign session cookies. Must be at least 32 characters. Generate one:
Terminal window
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
VariableRequiredDescription
AUTH_PROVIDERNopassword (default) or supabase. Omit to use password-only auth.

Used when AUTH_PROVIDER=password (or AUTH_PROVIDER is unset). All *_PASSWORD values are bcrypt hashes, not plaintext.

VariableRequiredDescription
ADMIN_PASSWORDYes (password mode)Bcrypt hash for the site owner account.
EDITOR_PASSWORDYes (password mode)Bcrypt hash for editor accounts.
VIEWER_<NAME>_PASSWORDNoBcrypt hash for a named viewer audience. <NAME> becomes the audience name (e.g. VIEWER_INTERNAL_PASSWORD). Optional — add one per gated viewer audience.
VIEWER_<NAME>_COLORNoHex color for the named audience badge in the editor UI (e.g. VIEWER_INTERNAL_COLOR=#10b981).
Terminal window
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.

Used when AUTH_PROVIDER=supabase.

VariableRequiredDescription
SUPABASE_URLYes (supabase mode)Your Supabase project URL, e.g. https://your-project.supabase.co.
SUPABASE_ANON_KEYYes (supabase mode)Supabase anon (public) key. Safe to include in the build.
SUPABASE_SERVICE_ROLE_KEYYes (supabase mode)Supabase service role key. Server-only — never expose to the client.

The optional SUPABASE_ACCOUNT_TOKEN and SUPABASE_PROJECT_REF variables are only needed when running Supabase migrations via the CLI (pnpm db:push) — they are not required at runtime.

Used on provisioned client sites instead of SUPABASE_SERVICE_ROLE_KEY. Brokers privileged operations (auth admin, video asset storage on Cloudflare R2, GitHub App writes) through the admin app instead of holding provider credentials on the client site.

VariableRequiredDescription
PLATFORM_API_URLYes (platform mode)Origin of the admin app’s broker API, e.g. https://admin.drawn.guide.
PLATFORM_API_KEYYes (platform mode)API key authorizing this site’s broker requests. Server-only — never expose to the client.
PORTAL_SITE_IDYes (platform mode)This site’s id in the shared platform database.

A site needs either this platform trio (the default for provisioned sites) or SUPABASE_SERVICE_ROLE_KEY (standalone) — not both. These three are usually set by the provisioner, not hand-filled.

Optional. Only needed for large, self-hosted video uploads (over 5 MB) — see Using video. External YouTube/Vimeo embeds and small looping video uploads (5 MB or less) work without any of this.

r2Assets() introduces no env vars on a provisioned (platform-mode) site: the admin app holds the Cloudflare R2 credentials and brokers uploads, deletes and listings, so the site needs only PLATFORM_API_URL + PLATFORM_API_KEY + PORTAL_SITE_ID.

A standalone site that talks to R2 directly needs six variables of its own:

VariableDescription
R2_ACCOUNT_IDCloudflare account id that owns the bucket.
R2_ACCESS_KEY_IDR2 S3-API access key id.
R2_SECRET_ACCESS_KEYR2 S3-API secret. Server-only. Scope the token to Object Read & Write on the one bucket — it never needs to create or delete buckets.
R2_BUCKETBucket name (portal-assets).
R2_PUBLIC_BASEPublic origin serving the bucket, e.g. https://assets.drawn.guide. Use a custom domain, not the r2.dev subdomain — Cloudflare rate-limits r2.dev and it gets no CDN caching.
PORTAL_SITE_IDThe site’s uuid. Every object key is tenant-prefixed sites/{PORTAL_SITE_ID}/…, so this is required in standalone mode too — not only in platform mode.

Optional. Only needed for document files over 2.5 MB — see Section types. Smaller PDFs and HTML files commit straight to the repository and need none of this.

Like r2Assets(), r2Documents() introduces no env vars on a provisioned (platform-mode) site: the admin app holds the credentials and brokers uploads, downloads and deletes.

A standalone site adds one variable to the four it already shares with the assets bucket (R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, PORTAL_SITE_ID):

VariableDescription
R2_DOCS_BUCKETName of a second, private R2 bucket for document files (portal-documents). It must have no public domain and no r2.dev access — documents are served only through the site’s auth-gated /api/document-file/ route, which signs a short-lived GET per request. There is deliberately no R2_DOCS_PUBLIC_BASE.
VariableRequiredDescription
SITEYes (Supabase mode, production)The canonical origin of the site, e.g. https://acme.drawn.guide. Required in Supabase mode — used by invite and password-reset emails. Not needed in password-only mode. Set in the Netlify dashboard, not in .env.

SITE must be pinned to the custom domain — not the *.netlify.app subdomain. It is used by invite and password-reset emails. Netlify’s built-in URL environment variable can resolve to the Netlify subdomain instead of the custom domain, so an explicit SITE is required.

This variable is set by the provisioner when creating the site. For existing sites built before this was introduced, set it manually in the Netlify dashboard and trigger a redeploy.

For the exhaustive list of every variable and its validation rules, see the Environment variable reference.