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.
GitHub
Section titled “GitHub”| Variable | Required | Description |
|---|---|---|
GITHUB_TOKEN | Yes | Fine-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_OWNER | Yes | GitHub organization or username that owns the client repo. |
GITHUB_REPO | Yes | Repository name (without the owner prefix). |
Auth core
Section titled “Auth core”| Variable | Required | Description |
|---|---|---|
SESSION_SECRET | Yes | Random string used to sign session cookies. Must be at least 32 characters. Generate one: |
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"| Variable | Required | Description |
|---|---|---|
AUTH_PROVIDER | No | password (default) or supabase. Omit to use password-only auth. |
Password mode
Section titled “Password mode”Used when AUTH_PROVIDER=password (or AUTH_PROVIDER is unset). All *_PASSWORD values are bcrypt hashes, not plaintext.
| Variable | Required | Description |
|---|---|---|
ADMIN_PASSWORD | Yes (password mode) | Bcrypt hash for the site owner account. |
EDITOR_PASSWORD | Yes (password mode) | Bcrypt hash for editor accounts. |
VIEWER_<NAME>_PASSWORD | No | Bcrypt 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>_COLOR | No | Hex color for the named audience badge in the editor UI (e.g. VIEWER_INTERNAL_COLOR=#10b981). |
Generating a bcrypt hash
Section titled “Generating a bcrypt hash”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”Used when AUTH_PROVIDER=supabase.
| Variable | Required | Description |
|---|---|---|
SUPABASE_URL | Yes (supabase mode) | Your Supabase project URL, e.g. https://your-project.supabase.co. |
SUPABASE_ANON_KEY | Yes (supabase mode) | Supabase anon (public) key. Safe to include in the build. |
SUPABASE_SERVICE_ROLE_KEY | Yes (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.
Platform mode
Section titled “Platform mode”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.
| Variable | Required | Description |
|---|---|---|
PLATFORM_API_URL | Yes (platform mode) | Origin of the admin app’s broker API, e.g. https://admin.drawn.guide. |
PLATFORM_API_KEY | Yes (platform mode) | API key authorizing this site’s broker requests. Server-only — never expose to the client. |
PORTAL_SITE_ID | Yes (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.
Assets (video storage)
Section titled “Assets (video storage)”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:
| Variable | Description |
|---|---|
R2_ACCOUNT_ID | Cloudflare account id that owns the bucket. |
R2_ACCESS_KEY_ID | R2 S3-API access key id. |
R2_SECRET_ACCESS_KEY | R2 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_BUCKET | Bucket name (portal-assets). |
R2_PUBLIC_BASE | Public 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_ID | The 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. |
Documents (PDF/HTML storage)
Section titled “Documents (PDF/HTML storage)”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):
| Variable | Description |
|---|---|
R2_DOCS_BUCKET | Name 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. |
Deploy
Section titled “Deploy”| Variable | Required | Description |
|---|---|---|
SITE | Yes (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.