Skip to content

Secrets & Environment

How App-name manages configuration across the two environments (test and production) using a single source of truth: GitHub holds the canonical secrets and injects them at deploy time into the server .env and containers. The Docker images stay environment-agnostic. For the complete variable tables see the Secrets Matrix and the Env Var Matrix.

The single-source-of-truth strategy

The principle is that no environment-specific value is baked into a build:

  1. Single definition. Each variable is defined once, in GitHub, scoped to the right environment.
  2. Runtime injection. At deploy time the workflow writes the server .env, and the frontend container generates its config.js at container startup from the environment it is given.
  3. Build agnosticism. The Docker image is built once and runs anywhere; it carries no <your-domain>- or test.<your-domain>-specific configuration.
flowchart LR
    GH[GitHub Secrets<br/>canonical] -->|deploy workflow| ENV[Server .env]
    ENV --> BE[Backend container]
    ENV --> FE[Frontend container<br/>generates config.js at startup]

GitHub secret hierarchy

Type Level Holds
Environment secrets production / test URLs, database strings, and API keys specific to that environment
Repository secrets Global SSH keys, deploy hosts, and cross-environment tokens (e.g. GOOGLE_SERVICE_ACCOUNT_JSON)

Required variables reference

The essentials are below. The full per-environment breakdown lives in the Secrets Matrix and Env Var Matrix.

Service keys

Variable Description Security rule
VITE_SUPABASE_URL Supabase project URL Public in the frontend
VITE_SUPABASE_ANON_KEY Anonymous key Public; obeys RLS
SUPABASE_SERVICE_KEY service_role key Critical. Never prefix with VITE_; bypasses RLS

Database connections

Variable Connection Port Use
DATABASE_URL Transaction pooler 6543 Standard app operations
DATABASE_URL_DIRECT Direct 5432 Migrations and heavy batch tasks

Application & external APIs

Variable Description Example / recommendation
VITE_API_URL Backend server URL https://<your-domain>
VITE_APP_URL Self-address (for email links) https://<your-domain>
SESSION_SECRET Auth encryption key openssl rand -base64 32
GOOGLE_SERVICE_ACCOUNT_JSON Google Gemini key Full text of the service-account .json

The service key is server-only

Anything prefixed VITE_ is bundled into the browser. If you ever prefix SUPABASE_SERVICE_KEY with VITE_, it becomes globally visible and anyone can bypass your Row Level Security. Never do this.

Local development .env

Locally you use a single .env at the repo root. To develop against the local Supabase stack, point these at the local ports instead — see Supabase (Local).

# .env (local)
NODE_ENV=development

# Supabase
VITE_SUPABASE_URL=https://xxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJI...
SUPABASE_SERVICE_KEY=eyJhbGciOiJI...

# Database
DATABASE_URL=postgresql://postgres.xxx:6543/postgres

# AI (local uses a file path)
GOOGLE_APPLICATION_CREDENTIALS=./config/key.json

# App
VITE_API_URL=http://localhost:5001
VITE_APP_URL=http://localhost:5173
SESSION_SECRET=local-dev-secret

GitHub secret configuration

Configure under Settings → Secrets and Variables → Actions.

Environment secrets

Create the production and test environments and add the per-environment values (Supabase URL/keys, DATABASE_URL, VITE_API_URL, VITE_APP_URL). These are the values the deploy workflow injects into the matching server's .env.

Repository secrets

Add these once for the whole project:

Secret Purpose
DEPLOY_HOST IP of the VPS
SSH_KEY Private deploy key
GOOGLE_SERVICE_ACCOUNT_JSON Optional: sync the Gemini service-account file
CF_ACCESS_CLIENT_ID Cloudflare Tunnel client ID
CF_ACCESS_CLIENT_SECRET Cloudflare Tunnel client secret

Security verification

Verify that fallbacks resolve before relying on a deploy:

cd backend
npm run test:config

Think of the two keys as hotel clearances:

Key Clearance Access
Anon key Guest card Only the user's own rows (RLS)
Service key Master key Every row and table (bypasses RLS)

Troubleshooting

Issue Fix
"Network Error" in the browser Confirm VITE_API_URL is the HTTPS domain of the server
Broken links in emails Confirm VITE_APP_URL matches the public website domain
AI models not responding Ensure GOOGLE_SERVICE_ACCOUNT_JSON holds the full, valid JSON