Secrets Management Guide¶
How secrets are managed across local development and remote environments.
Architecture¶
┌─────────────────────────────────────────────────────────────────────────┐
│ SECRETS ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ LOCAL DEVELOPMENT REMOTE ENVIRONMENTS (CI/CD Only) │
│ ───────────────── ─────────────────────────────── │
│ │
│ .env.local (gitignored) GitHub Secrets │
│ │ │ │
│ └─► Developer's machine ├─► SUPABASE_ACCESS_TOKEN │
│ ├─► SUPABASE_PROJECT_ID │
│ ├─► SUPABASE_PUBLISHABLE_KEY │
│ ├─► SERVICE_AUTH_SECRET_PRODUCTION │
│ └─► SERVICE_AUTH_SECRET_STAGING │
│ │ │
│ ▼ │
│ GitHub Actions Workflows │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ ▼ ▼ ▼ │
│ Production Staging Preview │
│ (main branch) (develop branch) (pr-* branch) │
│ │ │ │ │
│ └────────► supabase secrets set ◄──┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Key Principle¶
Remote secrets are managed exclusively through CI/CD.
Developers only need:
.env.localfor local development- Access to push code (CI/CD handles the rest)
Local Development Setup¶
Quick Start¶
# 1. Copy template
cp .env.local.example .env.local
# 2. Edit if needed (defaults work for most cases)
# 3. Start Supabase
supabase start
.env.local Contents¶
# Supabase local (defaults work automatically)
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_PUBLISHABLE_KEY=<auto-provided-by-supabase-start>
SUPABASE_SECRET_KEY=<auto-provided-by-supabase-start>
# Application
SERVICE_AUTH_SECRET=local-dev-secret
ENVIRONMENT=local
LOG_LEVEL=debug
# OAuth (optional - for local social login testing)
GITHUB_CLIENT_ID=your-local-oauth-app-id
GITHUB_CLIENT_SECRET=your-local-oauth-app-secret
GitHub Secrets Setup¶
Shared Repository Secrets¶
Add in Repository → Settings → Secrets and variables → Actions:
| Secret | Purpose | Source | How to Get |
|---|---|---|---|
SUPABASE_ACCESS_TOKEN |
CLI authentication | From Supabase | Dashboard → Account → Access Tokens |
SUPABASE_PROJECT_ID |
Project identifier | From Supabase | Dashboard → Settings → General → Reference ID |
SUPABASE_PUBLISHABLE_KEY |
API authentication | From Supabase | Dashboard → Settings → API → Project API keys |
Note: All three secrets above come from your Supabase project dashboard.
Environment-Specific Secrets¶
Production Environment (Settings → Environments → production):
| Secret | Purpose | Used By | How to Generate |
|---|---|---|---|
SERVICE_AUTH_SECRET_PRODUCTION |
Custom app secret for service-to-service auth (Frontend ↔ Backend authentication) |
main branch → Supabase main branch |
openssl rand -base64 32You generate this, not from Supabase |
Staging Environment (Settings → Environments → staging):
| Secret | Purpose | Used By | How to Generate |
|---|---|---|---|
SERVICE_AUTH_SECRET_STAGING |
Custom app secret for service-to-service auth (Frontend ↔ Backend authentication) |
develop branch → Supabase develop branchAlso used as seed for PR preview secrets |
openssl rand -base64 32You generate this, not from Supabase |
What is SERVICE_AUTH_SECRET?¶
SERVICE_AUTH_SECRET is a custom application secret (not from Supabase)
used to authenticate requests between your frontend (Vercel/Next.js) and backend
(Supabase Edge Functions):
Security Flow:
1. User → Vercel Edge (rate limiting, WAF protection)
2. Next.js adds header: X-Service-Auth: <SERVICE_AUTH_SECRET>
3. Edge Function validates: header matches SERVICE_AUTH_SECRET
4. Direct public requests without valid secret are rejected
Why this matters:
- Prevents direct access to Supabase Edge Functions from public internet
- Routes all traffic through Vercel's rate limiting infrastructure
- Ensures only your authorized frontend can call your backend APIs
Important: The same secret must be set in:
- GitHub Environment Secret → used by CI/CD to set Supabase branch secret
- Vercel Environment Variable → used by Next.js to add authentication header
- Must match on both sides for requests to succeed
Security Benefits of Environment Secrets¶
- Isolation: Staging workflows can't accidentally access production secrets
- Approval: Production environment can require reviewers before deployment
- Audit Trail: GitHub logs which environment each secret was accessed from
- Risk Reduction: Compromised staging secret doesn't affect production
Branch-to-Secret Mapping¶
Persistent Branches¶
| Git Branch | Supabase Branch | GitHub Environment | SERVICE_AUTH_SECRET Source |
|---|---|---|---|
main |
main (production) |
production |
SERVICE_AUTH_SECRET_PRODUCTION |
develop |
develop (staging) |
staging |
SERVICE_AUTH_SECRET_STAGING |
Ephemeral Branches (PR Previews)¶
| Git Branch | Supabase Branch | GitHub Environment | SERVICE_AUTH_SECRET Source |
|---|---|---|---|
feat/*, fix/*, etc. |
pr-{number} (preview) |
preview-pr-{number} |
Auto-generated per PR |
How PR secrets are generated:
# Unique secret per PR = SHA256(branch_name + staging_secret)
PREVIEW_SECRET=$(echo -n "pr-123-${SERVICE_AUTH_SECRET_STAGING}" | sha256sum)
This ensures:
- Each PR preview has a unique, isolated secret
- Secrets are deterministic (same PR always gets same secret)
- Staging secret compromise doesn't expose all PR secrets
- PR secrets are automatically deleted when branch is deleted
Secret Generation¶
# Generate secure random secrets
openssl rand -base64 32
# Or use Deno task
deno task secrets:generate
How GitHub Actions Finds Secrets¶
When a workflow job runs:
- If job has
environmentspecified: Look for secrets in that environment first - If secret not found in environment: Look in repository secrets (fallback)
- If not found: Workflow fails with "secret not found" error
Example:
deploy-production.ymljob hasenvironment: production- When it references
${{ secrets.SERVICE_AUTH_SECRET_PRODUCTION }}, GitHub first looks in the production environment secrets - Shared secrets like
${{ secrets.SUPABASE_ACCESS_TOKEN }}fall back to repository secrets
This allows environment-specific protection while sharing credentials across environments.
How CI/CD Sets Secrets¶
Production Deployment¶
When code is pushed to main, deploy-production.yml:
- Links to Supabase project
- Sets production secrets:
- Deploys Edge Functions
- Runs health checks
Staging Deployment¶
When code is pushed to develop, deploy-staging.yml:
- Links to Supabase project
- Sets staging secrets on develop branch:
- Deploys to staging branch
- Runs health checks
Preview Deployment¶
When a PR has the deploy-preview label, pr-checks.yml:
- Creates ephemeral branch
pr-{number} - Sets preview secrets:
- Deploys to preview branch
- Comments preview URL on PR
Complete Secret Flow Reference¶
| Branch Type | Git Branch | Supabase Branch | GitHub Env | GitHub Secret Used | Supabase Secret Set |
|---|---|---|---|---|---|
| Production | main |
main |
production |
SERVICE_AUTH_SECRET_PRODUCTION |
SERVICE_AUTH_SECRET=xxx |
| Staging | develop |
develop |
staging |
SERVICE_AUTH_SECRET_STAGING |
SERVICE_AUTH_SECRET=yyy |
| PR Preview | feat/* |
pr-{N} |
preview-pr-{N} |
SERVICE_AUTH_SECRET_STAGING (seed) |
SERVICE_AUTH_SECRET=zzz (auto-generated) |
Key Points:
- Production and staging have dedicated, manually-set secrets in GitHub Environments
- PR previews auto-generate unique secrets derived from staging secret
- All secrets are set as Supabase branch secrets (via
supabase secrets set --branch) - Ephemeral PR branches and their secrets are automatically deleted when PR closes
Secret Types¶
Project-Level GitHub Secrets (Used by CI/CD)¶
| Secret | Set In | Used By |
|---|---|---|
SUPABASE_ACCESS_TOKEN |
GitHub Secrets | CLI operations (deploy, migrations) |
SUPABASE_PROJECT_ID |
GitHub Secrets | Project identification |
SUPABASE_PUBLISHABLE_KEY |
GitHub Secrets | API calls (smoke tests, health checks) |
Note: SUPABASE_SECRET_KEY (Supabase's service role key) is currently NOT
used:
Current Architecture:
- ✅ All Edge Function operations use
SUPABASE_PUBLISHABLE_KEY+ user authentication - ✅ Row Level Security (RLS) is enforced on all database operations
- ✅ No admin/privileged operations at runtime
When to add it: If you need Edge Functions to perform admin operations (create users, bypass RLS, etc.):
-
Add to GitHub Secrets (repository level):
-
Update workflows to set it as Supabase branch secret:
-
Use in code:
Currently used in:
- Local scripts only (
.env.local): seed-curriculum.ts, analyze-state.ts, verify-snapshot.ts - NOT used in production Edge Functions
Branch-Level (Different per Environment)¶
| Secret | Production | Staging | Preview |
|---|---|---|---|
ENVIRONMENT |
production |
staging |
preview |
LOG_LEVEL |
info |
debug |
debug |
SERVICE_AUTH_SECRET |
Unique | Unique | Auto-generated |
Security Best Practices¶
Do ✅¶
- Store remote secrets only in GitHub Secrets
- Use unique
SERVICE_AUTH_SECRETper environment - Generate secrets with
openssl rand -base64 32(minimum 32 chars) - Rotate secrets quarterly
- Use GitHub environment protection for production
Don't ❌¶
- Store production secrets locally (
.env.productionfiles) - Share secrets via Slack, email, or PR comments
- Reuse the same secret across environments
- Commit any
.envfiles (check.gitignore)
Rotating Secrets¶
When to Rotate¶
- Quarterly (recommended schedule)
- When a team member leaves
- If a secret may have been exposed
How to Rotate¶
-
Generate new secret:
-
Update GitHub Secrets:
-
Repository → Settings → Secrets → Update secret
-
Trigger redeployment:
-
Update dependent services:
- Frontend (Vercel): Update
SERVICE_AUTH_SECRET - Any other services using this secret
Verifying Secrets¶
Check Supabase Secrets¶
# Link project first
supabase link --project-ref <project-id>
# List production secrets
supabase secrets list
# List staging secrets
supabase secrets list --branch develop
# List preview secrets
supabase secrets list --branch pr-123
Verify in CI/CD¶
Check GitHub Actions workflow logs for secret-setting steps.
Troubleshooting¶
"Missing required secret" in CI¶
- Go to Repository → Settings → Secrets
- Verify secret name matches exactly (case-sensitive)
- Ensure secret value is not empty
"Invalid SERVICE_AUTH_SECRET" at runtime¶
- Check secrets are set on correct branch:
- Verify secret matches between Supabase and frontend
- Redeploy to refresh secrets
Local auth fails¶
- Verify
.env.localexists and has values - Check Supabase is running:
supabase status - For OAuth: verify you have separate localhost OAuth apps
Secrets not updating after change¶
- GitHub Actions caches may need clearing
- Trigger a fresh deployment
- For Supabase: secrets apply on next function invocation
Commands Reference¶
# Generate secure secret
openssl rand -base64 32
deno task secrets:generate
# List current Supabase secrets
supabase secrets list
supabase secrets list --branch develop
supabase secrets list --branch pr-123
# Manual secret set (emergency only - prefer CI/CD)
supabase secrets set KEY=value
supabase secrets set --branch develop KEY=value
Related Documentation¶
- Multi-Environment Configuration - Full config reference
- CI/CD Guide - GitHub Actions workflows
- Branching Setup - Branch strategy