Environment Variables
Complete reference for all environment variables used across the Loop Health platform.
Database
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string |
SUPABASE_URL | Yes | Supabase project URL |
SUPABASE_ANON_KEY | Yes | Supabase anonymous (public) key |
SUPABASE_SERVICE_KEY | Yes | Supabase service role key (server-side only) |
Authentication (Clerk)
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY | Yes | Clerk publishable key (client-side) |
CLERK_SECRET_KEY | Yes | Clerk secret key (server-side) |
CLERK_ISSUER_URL | Yes | Clerk issuer URL for JWT verification |
AI Providers
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY | Luna, Embeddings | OpenAI API key |
ANTHROPIC_API_KEY | Luna, Biomarker Parser | Anthropic API key |
SSO
| Variable | Required | Description |
|---|---|---|
SSO_PRIVATE_KEY | Admin | SSO private key for token signing |
SSO_PUBLIC_KEY | Consumer | SSO public key for token verification |
SSO_JWT_SECRET | Both | Shared secret for HS256 SSO tokens |
JWT_SECRET | API | General JWT secret |
Wearables
| Variable | Required | Description |
|---|---|---|
OURA_CLIENT_ID | Consumer | Oura OAuth client ID |
OURA_CLIENT_SECRET | Consumer | Oura OAuth client secret |
WHOOP_CLIENT_ID | Consumer | Whoop OAuth client ID |
WHOOP_CLIENT_SECRET | Consumer | Whoop OAuth client secret |
WHOOP_REDIRECT_URI | Consumer | Whoop OAuth redirect URI |
DEXCOM_CLIENT_ID | Consumer | Dexcom OAuth client ID |
DEXCOM_CLIENT_SECRET | Consumer | Dexcom OAuth client secret |
LIBRE_CLIENT_ID | Consumer | Libre OAuth client ID |
LIBRE_CLIENT_SECRET | Consumer | Libre OAuth client secret |
Background Jobs (Trigger.dev)
| Variable | Required | Description |
|---|---|---|
TRIGGER_API_KEY | Trigger | Trigger.dev API key |
TRIGGER_API_URL | Trigger | Trigger.dev API URL |
TRIGGER_PROJECT_ID | Trigger | Trigger.dev project ID |
Activity Feeds (GetStream)
| Variable | Required | Description |
|---|---|---|
STREAM_API_KEY | Consumer | GetStream API key |
STREAM_API_SECRET | Consumer | GetStream API secret |
Notifications (Knock)
| Variable | Required | Description |
|---|---|---|
KNOCK_SECRET_KEY | Consumer | Knock secret key |
KNOCK_API_KEY | Consumer | Knock API key |
KNOCK_SIGNING_KEY | Consumer | Knock webhook signing key |
Commerce (BigCommerce)
| Variable | Required | Description |
|---|---|---|
BIGCOMMERCE_STORE_HASH | Consumer | BigCommerce store hash |
BIGCOMMERCE_ACCESS_TOKEN | Consumer | BigCommerce API access token |
BIGCOMMERCE_API_URL | Consumer | BigCommerce API URL |
Commerce (Bolt)
| Variable | Required | Description |
|---|---|---|
BOLT_API_KEY | Consumer | Bolt checkout API key |
BOLT_PUBLISHABLE_KEY | Consumer | Bolt publishable key |
Rimo Health
| Variable | Required | Description |
|---|---|---|
RIMO_WEBHOOK_SECRET | Patient Graph | Rimo webhook HMAC secret |
RIMO_API_URL | Consumer | Rimo Health API URL |
RIMO_API_KEY | Consumer | Rimo Health API key |
Voice (Vapi)
| Variable | Required | Description |
|---|---|---|
VAPI_API_KEY | Luna | Vapi API key for voice calls |
VAPI_WEBHOOK_SECRET | Luna | Vapi webhook secret |
Monitoring
| Variable | Required | Description |
|---|---|---|
SENTRY_DSN | All | Sentry DSN for error tracking |
SENTRY_AUTH_TOKEN | CI | Sentry auth token for source maps |
NEXT_PUBLIC_POSTHOG_KEY | Consumer | PostHog project key |
NEXT_PUBLIC_POSTHOG_HOST | Consumer | PostHog host URL |
Caching (Upstash Redis)
| Variable | Required | Description |
|---|---|---|
UPSTASH_REDIS_REST_URL | Core | Upstash Redis REST URL |
UPSTASH_REDIS_REST_TOKEN | Core | Upstash Redis REST token |
Cron Jobs
| Variable | Required | Description |
|---|---|---|
CRON_SECRET | Consumer | Secret for authenticating cron requests |
Platform API
| Variable | Required | Description |
|---|---|---|
PLATFORM_API_KEY | Admin, Consumer | API key for platform internal API |
Patient Graph API
| Variable | Required | Description |
|---|---|---|
PATIENT_GRAPH_API_URL | Consumer, Luna | Patient Graph API base URL |
PATIENT_GRAPH_API_KEY | Consumer, Luna | Patient Graph API authentication key |
Junction Health (Wearables)
| Variable | Required | Description |
|---|---|---|
JUNCTION_API_KEY | Patient Graph | Junction Health API key for unified wearables |
JUNCTION_API_URL | Patient Graph | Junction Health API base URL |
Environment-Specific Values
Development
# .env.local
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/loop_dev"
SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_ANON_KEY="eyJ..."
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
CLERK_SECRET_KEY="sk_test_..."
OPENAI_API_KEY="sk-..."
UPSTASH_REDIS_REST_URL="https://localhost:8079"
UPSTASH_REDIS_REST_TOKEN="local"Staging
# Vercel staging environment
DATABASE_URL="postgresql://user:pass@staging.supabase.co:5432/loop_staging?pool=1&pgbouncer=true"
SUPABASE_URL="https://staging-project.supabase.co"
SUPABASE_ANON_KEY="eyJ..."
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_..."
CLERK_SECRET_KEY="sk_test_..."
CLERK_ISSUER_URL="https://staging.clerk.loop.health"
SENTRY_DSN="https://...@sentry.io/staging"
SENTRY_ENVIRONMENT="staging"Production
# Vercel production environment
DATABASE_URL="postgresql://user:pass@prod.supabase.co:5432/loop_production?pool=1&pgbouncer=true"
SUPABASE_URL="https://prod-project.supabase.co"
SUPABASE_ANON_KEY="eyJ..."
SUPABASE_SERVICE_KEY="eyJ..."
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_live_..."
CLERK_SECRET_KEY="sk_live_..."
CLERK_ISSUER_URL="https://clerk.loop.health"
SENTRY_DSN="https://...@sentry.io/production"
SENTRY_ENVIRONMENT="production"
SENTRY_RELEASE="${VERCEL_GIT_COMMIT_SHA}"Security Best Practices
Secret Rotation Schedule
| Secret Type | Rotation Frequency | Owner | Priority |
|---|---|---|---|
| Database passwords | 90 days | DevOps | High |
| API keys (external services) | 180 days | Engineering | Medium |
| JWT signing keys | 365 days | Security | Critical |
| Webhook secrets | On breach only | Engineering | High |
| OAuth client secrets | 180 days | Engineering | Medium |
Rotation Procedure
# 1. Generate new secret
NEW_SECRET=$(openssl rand -base64 32)
# 2. Set in Vercel (keep old value temporarily)
vercel env add DATABASE_PASSWORD production
# Paste new value
# 3. Deploy with new secret
vercel --prod
# 4. Verify deployment healthy
curl https://my.loop.health/api/health
# 5. Remove old secret value
# (Old value kept for rollback during grace period)Secret Storage
DO ✅:
- Use Vercel environment variables for runtime secrets
- Use GitHub Secrets for CI/CD secrets
- Rotate secrets on schedule
- Use different secrets per environment
- Audit secret access logs
- Use service accounts (not personal API keys)
- Enable MFA on secret management accounts
DON’T ❌:
- Commit secrets to Git (even in .env files)
- Share secrets via Slack/email
- Use same secret across environments
- Use personal API keys in production
- Hard-code secrets in source code
- Log secrets (even in debug mode)
- Store secrets in unencrypted files
Secret Validation
// Validate required secrets at startup
const requiredSecrets = [
'DATABASE_URL',
'CLERK_SECRET_KEY',
'SENTRY_DSN',
];
for (const secret of requiredSecrets) {
if (!process.env[secret]) {
throw new Error(`Missing required environment variable: ${secret}`);
}
}
// Validate secret format
if (!process.env.DATABASE_URL?.startsWith('postgresql://')) {
throw new Error('DATABASE_URL must be a valid PostgreSQL connection string');
}Connection String Patterns
PostgreSQL (Supabase)
Format:
postgresql://[user]:[password]@[host]:[port]/[database]?[params]Required parameters:
pool=1- Single connection per instance (Vercel Edge Functions)pgbouncer=true- Use Supabase connection pooler
Example:
DATABASE_URL="postgresql://postgres.abc123:password@aws-0-us-east-1.pooler.supabase.com:5432/postgres?pool=1&pgbouncer=true"Why these params?
- Vercel Edge Functions are serverless and ephemeral
- Each function invocation creates a new connection
- Without pooling, you hit connection limits quickly
- Supabase pgbouncer pools connections efficiently
Redis (Upstash)
Format:
https://[id].upstash.ioExample:
UPSTASH_REDIS_REST_URL="https://abc-123-xyz.upstash.io"
UPSTASH_REDIS_REST_TOKEN="AX..."Clerk (Authentication)
Issuer URL format:
https://[subdomain].clerk.accounts.dev # Development
https://clerk.[yourdomain].com # ProductionExample:
CLERK_ISSUER_URL="https://clerk.loop.health"
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_live_..."
CLERK_SECRET_KEY="sk_live_..."Troubleshooting
Missing Environment Variables
Symptom: Runtime error process.env.X is undefined
Fix:
# 1. Check if variable is set in Vercel
vercel env ls
# 2. Add missing variable
vercel env add MISSING_VAR production
# 3. Redeploy
vercel --prod
# 4. Verify in deployment logs
vercel logs [deployment-url]Database Connection Errors
Symptom: Connection pool exhausted or ECONNREFUSED
Common causes:
- Missing
pool=1&pgbouncer=trueparameters - Wrong database host/port
- Firewall blocking connections
- Database credentials expired
Fix:
# Verify connection string format
echo $DATABASE_URL | grep "pool=1" | grep "pgbouncer=true"
# Test connection manually
psql "$DATABASE_URL" -c "SELECT 1;"
# Check Supabase pooler status
# Dashboard → Database → Connection PoolingClerk Authentication Failures
Symptom: Invalid JWT signature or Issuer mismatch
Common causes:
- Wrong
CLERK_ISSUER_URL(must match Clerk dashboard) - Mismatched publishable key and secret key
- Clock skew (JWT exp/iat validation)
Fix:
# Verify issuer URL matches Clerk dashboard
# Dashboard → API Keys → Issuer
# Ensure keys match environment
# pk_test_* → sk_test_*
# pk_live_* → sk_live_*
# Check system time
date -u # Should be within 5 minutes of actual UTCAPI Key Validation Errors
Symptom: 401 Unauthorized from external API
Common causes:
- API key expired
- Wrong API key for environment (test vs prod)
- API key format incorrect (missing prefix)
- Rate limit exceeded
Fix:
# Test API key manually
curl -H "Authorization: Bearer $OPENAI_API_KEY" \
https://api.openai.com/v1/models
# Check key format
# OpenAI: sk-...
# Anthropic: sk-ant-...
# Rimo: rm_...
# Rotate key if expired
# (See Secret Rotation above)Per-Service Configuration
my.loop.health (Consumer App)
Platform: Vercel Environment Variables: 25+
Critical variables:
DATABASE_URL- Supabase PostgreSQLNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY- Clerk auth (public)CLERK_SECRET_KEY- Clerk auth (secret)PATIENT_GRAPH_API_URL- Patient Graph APIPATIENT_GRAPH_API_KEY- Patient Graph authSENTRY_DSN- Error trackingUPSTASH_REDIS_REST_URL- CachingUPSTASH_REDIS_REST_TOKEN- Caching auth
Setup:
cd apps/my-loop-health
vercel link
vercel env pull .env.localapps/patient-graph (Patient Graph API)
Platform: Vercel (or Render/Fly.io - see deployment docs) Environment Variables: 15+
Critical variables:
DATABASE_URL- Supabase PostgreSQL (health project)CLERK_ISSUER_URL- JWT validationCLERK_SECRET_KEY- JWT validationRIMO_WEBHOOK_SECRET- Rimo webhook validationJUNCTION_API_KEY- Wearables APISENTRY_DSN- Error trackingUPSTASH_REDIS_REST_URL- CachingUPSTASH_REDIS_REST_TOKEN- Caching auth
Setup:
cd apps/patient-graph
vercel link
vercel env pull .env.localapps/admin (Admin Dashboard)
Platform: Vercel Environment Variables: 20+
Critical variables:
DATABASE_URL- Supabase PostgreSQL (both projects)NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY- Clerk authCLERK_SECRET_KEY- Clerk authSSO_PRIVATE_KEY- SSO token signingPLATFORM_API_KEY- Internal API authSENTRY_DSN- Error tracking
Setup:
cd apps/admin
vercel link
vercel env pull .env.localVerification Checklist
Before deploying to production, verify:
- All required variables set in Vercel environment
- Database connection string includes
pool=1&pgbouncer=true - Clerk keys match environment (test vs live)
- Sentry DSN points to correct project
- API keys are production (not test) keys
- Webhook secrets match external service configuration
- Redis credentials are production (not local)
- All secrets different from staging
- No secrets committed to Git
- Secrets rotation schedule documented
- Test deployment with
vercel --prod(from branch) - Verify health check passes:
/api/health - Check Sentry for deployment errors