Skip to content

Secrets & Variables Matrix

Every GitHub Actions secret, GitHub variable, and server-side .env value that the app-name pipeline touches — split by where it must live. The guiding rule: deployment plumbing lives in GitHub; runtime application secrets live only on the server.

Single source of truth

Each value is defined in exactly one place. Frontend (VITE_*) and runtime secrets are defined once in the server .env and injected into the container's config.js at startup — the image itself is environment-agnostic. GitHub only holds what the pipeline needs to build, scan, and reach the server. See Secrets & Environment.


GitHub Secrets

Encrypted values in Settings → Secrets and variables → Actions. Referenced as ${{ secrets.NAME }}.

Name Scope Purpose Notes
GITHUB_TOKEN repo (auto) Auto-provided token. Authenticates docker login ghcr.io to push images (build) and pull them on the server (deploy). Also used by the SonarCloud scan. Injected, never set by hand. Workflow grants packages: write, contents: read, id-token: write.
SONAR_TOKEN repo Authenticates the SonarCloud quality-gate scan and the gate-status API check. Only consumed when vars.SONAR_ENABLED == 'true'.
DEPLOY_HOST repo Hostname/IP of the test server, reached over the Cloudflare Tunnel. Workflow falls back to vars.DEPLOY_HOST if the secret is unset (secrets.DEPLOY_HOST || vars.DEPLOY_HOST).
PROD_DEPLOY_HOST repo Hostname/IP of the production server. Falls back to vars.PROD_DEPLOY_HOST.
SSH_KEY repo Private SSH deploy key used to reach the servers. Written to ~/.ssh/id_rsa in the runner. Falls back to vars.SSH_KEY. Must include full BEGIN/END lines.
PROD_SSH_KEY repo Optional production-specific SSH key. Resolution order: secrets.PROD_SSH_KEY || secrets.SSH_KEY || vars.SSH_KEY. Omit to reuse SSH_KEY.
CF_ACCESS_CLIENT_ID repo Cloudflare Access service-token ID. Feeds cloudflared access ssh as the SSH ProxyCommand. Shared across test and production.
CF_ACCESS_CLIENT_SECRET repo Cloudflare Access service-token secret. Shared across test and production. See Cloudflare Tunnel.

Secret-or-variable fallback pattern

Hosts and keys use secrets.X || vars.X so non-sensitive deployments can store the host as a plain variable while sensitive setups keep it as a secret. Pick one per value and stay consistent.

Optional / situational secrets

Name Scope Purpose Notes
SKILLS_READ_TOKEN repo Read token for pulling shared internal skills/packages during CI. Only required if the build consumes a private skills source.
CLOUDFLARE_API_TOKEN repo Cloudflare API token for managing Pages deploys / DNS from CI. Only needed if the frontend is published via Cloudflare Pages.
CLOUDFLARE_ACCOUNT_ID repo Cloudflare account ID paired with the API token. Often kept as a var rather than a secret.

GitHub Variables (vars)

Plain, non-secret values in the Variables tab. Referenced as ${{ vars.NAME }}.

Name Scope Purpose Notes
SONAR_ENABLED repo Feature flag — gates the entire SonarCloud quality-gate job (if: vars.SONAR_ENABLED == 'true'). Set to true to enforce the gate; leave unset/false to skip it (the gate then resolves as skipped, which still allows deploy).
CF_PAGES_PROJECT repo Cloudflare Pages project name, if the frontend is deployed to Pages. Pairs with CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID.
DEPLOY_HOST repo / environment Fallback test host when not stored as a secret. Used via secrets.DEPLOY_HOST || vars.DEPLOY_HOST.
PROD_DEPLOY_HOST repo / environment Fallback production host. Used via secrets.PROD_DEPLOY_HOST || vars.PROD_DEPLOY_HOST.
SSH_KEY repo Fallback SSH key location when not stored as a secret. Storing a private key as a plain variable is discouraged — prefer the secret.

Server-side .env (never in GitHub)

These runtime application secrets live only in the server .env (/opt/app-name/.env for test, /opt/app-name-prod/.env for production). The deploy script aborts if .env is absent. They are deliberately not stored as GitHub secrets.

Name Scope Purpose Notes
DATABASE_URL server .env App database connection (transaction pooler, port 6543). Per-environment value; same key name everywhere.
DATABASE_URL_DIRECT server .env Direct connection (port 5432) for migrations.
DB_SSL_MODE server .env Postgres SSL mode, e.g. require.
VITE_SUPABASE_URL server .env Supabase URL injected into config.js at container start. Public, but still managed on the server for single-source consistency.
VITE_SUPABASE_ANON_KEY server .env Supabase anon key. Public; obeys RLS.
SUPABASE_SERVICE_KEY server .env service_role key — bypasses RLS. Critical. Never prefix with VITE_.
SENDGRID_API_KEY server .env SendGrid transactional email key.
SENDGRID_FROM_EMAIL server .env Verified sender address.
SESSION_SECRET server .env Session signing/encryption secret. openssl rand -base64 32.
VITE_API_URL / VITE_APP_URL server .env Public API and app URLs. Must match the environment's domain.

Do not promote runtime secrets into GitHub

Putting DATABASE_URL, SUPABASE_SERVICE_KEY, SESSION_SECRET, or any API key into GitHub Secrets breaks the single-source-of-truth model and widens the blast radius of a leak. The pipeline is designed to need none of them — it only injects IMAGE_TAG, ENV, REGISTRY_OWNER, GITHUB_TOKEN, and GITHUB_ACTOR via the transient .env.deploy.


GitHub Environments

Defined under Settings → Environments. Names must match the workflow environment: keys exactly.

Environment Used by Trigger Protection rules
test deploy.ymldeploy-test job Automatic on push to main (also workflow_dispatch). None required. Deploy runs only after the quality-gate, E2E-results, and security-audit jobs succeed (or the Sonar gate is skipped).
production deploy-production.ymldeploy-production job Push of a v* tag or manual workflow_dispatch. Recommended: required reviewers on the production environment so deploys pause for manual approval. Same gate jobs must pass first.

Gates precede the environment

Both workflows make the deploy job needs: the gate jobs with an if: guard. The environment's own protection rules (e.g. required reviewers) apply on top of those gates, giving production a manual checkpoint.