environment variables
Complete reference for all environment variables used by the owlette web dashboard.
firebase client (required)
These are exposed to the browser (client-side). The NEXT_PUBLIC_ prefix is required by Next.js.
| variable | example | source |
|---|---|---|
NEXT_PUBLIC_FIREBASE_API_KEY | AIzaSy... | Firebase Console → Project Settings → Web App |
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN | my-project.firebaseapp.com | Same |
NEXT_PUBLIC_FIREBASE_PROJECT_ID | my-project-id | Same |
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET | my-project.appspot.com | Same |
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID | 123456789012 | Same |
NEXT_PUBLIC_FIREBASE_APP_ID | 1:123:web:abc | Same |
These values are public by design — Firebase client SDKs are designed to be used in browsers. Security is enforced by Firestore security rules, not by keeping these values secret.
firebase admin (required)
Server-side only — used for generating agent OAuth tokens and verifying sessions. Use three separate variables (not a JSON blob).
| variable | format | source |
|---|---|---|
FIREBASE_PROJECT_ID | my-project-id | Firebase Console → Project Settings → General |
FIREBASE_CLIENT_EMAIL | firebase-adminsdk-xxx@my-project.iam.gserviceaccount.com | Firebase Console → Service Accounts → Generate Key |
FIREBASE_PRIVATE_KEY | "-----BEGIN PRIVATE KEY-----\n..." | Same — keep the \n escape sequences |
When setting FIREBASE_PRIVATE_KEY in Railway, wrap the value in double quotes and preserve the \n newline escapes exactly as exported from Firebase.
The web runtime reads the split variables above from web/lib/firebase-admin.ts. FIREBASE_SERVICE_ACCOUNT_KEY JSON blobs are legacy documentation only for this app and are not read by the current server code.
session management (required)
| variable | format | description |
|---|---|---|
SESSION_SECRET | 32+ character string | Encryption key for iron-session HTTPOnly cookies |
MFA_ENCRYPTION_KEY | 32+ character string | Encryption key for 2FA secrets stored in Firestore |
Generate a secure value:
python -c "import secrets; print(secrets.token_hex(32))"email (required for alerts)
| variable | description |
|---|---|
RESEND_API_KEY | API key from Resend for sending emails |
RESEND_FROM_EMAIL | Verified sender address (e.g. alerts@owlette.app) |
ADMIN_EMAIL_PROD | Fallback admin email address (production) |
ADMIN_EMAIL_DEV | Fallback admin email address (development) |
SEND_WELCOME_EMAIL | true or false — controls welcome emails to new users |
rate limiting (required)
Owlette uses Upstash Redis for API rate limiting. Create a free Serverless Redis database and copy the REST connection details.
| variable | description |
|---|---|
UPSTASH_REDIS_REST_URL | REST API URL for your Upstash Redis instance |
UPSTASH_REDIS_REST_TOKEN | Authentication token for Upstash Redis |
cloudflare r2 (required for roost)
The roost upload, chunk-check, and version-body routes use Cloudflare R2 through its S3-compatible API. The web runtime reads these variables in web/lib/r2Client.server.ts.
| variable | format | description |
|---|---|---|
R2_S3_ENDPOINT | https://<account-id>.r2.cloudflarestorage.com | S3-compatible endpoint. This is where the Cloudflare account id is configured at runtime. |
R2_S3_ACCESS_KEY_ID | R2 access key id | S3-compatible R2 token with Object Read & Write access to the owlette buckets |
R2_S3_SECRET_ACCESS_KEY | R2 secret access key | Matching secret for R2_S3_ACCESS_KEY_ID |
Bucket names are not environment variables in the current web code. bucketFor() derives them from the active roost environment:
| roost env | content bucket | manifests bucket |
|---|---|---|
prod | owlette-prod-content | owlette-prod-manifests |
dev | owlette-dev-content | owlette-dev-manifests |
ROOST_ENV=prod or ROOST_ENV=dev can override environment detection. Without that override, production is selected when RAILWAY_ENVIRONMENT=production or RAILWAY_PUBLIC_DOMAIN=owlette.app; otherwise the code defaults to dev.
There is no separate public URL prefix runtime variable. R2 buckets should remain private; upload and download access uses presigned URLs. Version metadata URLs are derived from the selected manifests bucket and R2_S3_ENDPOINT.
CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_R2_API_TOKEN are provisioning inputs for scripts/provision-r2.mjs, not web runtime variables.
url configuration (required in production)
| variable | description |
|---|---|
NEXT_PUBLIC_BASE_URL | Public base URL used in email links and agent callbacks (e.g. https://owlette.app) |
RAILWAY_PUBLIC_DOMAIN | Railway deployment domain — auto-injected by Railway, override if using a custom domain |
cron (required for health checks)
| variable | description |
|---|---|
CRON_SECRET | Shared secret for authenticating cron health-check requests |
Generate:
python -c "import secrets; print(secrets.token_hex(32))"public status page (required for external launch)
These are server-side only. They wire /api/cron/status-ping to the hosted public status page.
| variable | description |
|---|---|
OWLETTE_STATUS_BASE_URL | Optional base URL for synthetic checks; defaults to the request origin or https://owlette.app |
INSTATUS_API_KEY | Instatus API token |
INSTATUS_PAGE_ID | Instatus hosted page id; not tied to the later status.owlette.app custom-domain upgrade |
INSTATUS_API_BASE_URL | Optional API override; defaults to https://api.instatus.com |
INSTATUS_COMPONENT_STATUS_METHOD | Optional component update method; defaults to PUT |
INSTATUS_COMPONENT_STATUS_URL_TEMPLATE | Optional component update URL template using {pageId} and {componentId} placeholders; default is /v2/{pageId}/components/{componentId} under INSTATUS_API_BASE_URL |
INSTATUS_COMPONENT_DASHBOARD_ID | Component id for dashboard |
INSTATUS_COMPONENT_API_ID | Component id for API |
INSTATUS_COMPONENT_AGENT_REGISTRY_ID | Component id for agent registry |
INSTATUS_COMPONENT_WEBHOOK_DELIVERY_ID | Component id for webhook delivery |
INSTATUS_COMPONENT_R2_UPLOADS_ID | Component id for R2 uploads |
INSTATUS_COMPONENT_FIRESTORE_ID | Component id for Firestore |
INSTATUS_COMPONENT_CORTEX_CHAT_ID | Component id for Cortex chat |
Do not store the Instatus API key in docs or screenshots. Record only component ids and env var names in the operator reference.
public API launch state
There is no environment variable that enables the public API launch. The launch state is tracked in the public API launch ticket and runbook, while API routes continue to enforce authentication, scopes, rate limits, and audit behavior.
Do not add an OWLETTE_PUBLIC_API_LAUNCHED style flag unless the web code also enforces that flag on the relevant routes and the rollback behavior is documented.
encryption (required for llm keys)
| variable | description |
|---|---|
LLM_ENCRYPTION_KEY | 32-byte hex key for encrypting stored LLM API keys |
Generate:
python -c "import secrets; print(secrets.token_hex(32))"autonomous cortex (optional)
| variable | description |
|---|---|
CORTEX_INTERNAL_SECRET | Shared secret for internal auth between alert route and autonomous Cortex endpoint |
Generate:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Required only if you want autonomous Cortex (AI auto-investigates process crashes). Also requires a site-level LLM API key configured in Firestore (sites/{siteId}/settings/llm) and autonomous mode enabled (sites/{siteId}/settings/cortex → autonomousEnabled: true).
observability (optional)
| variable | description |
|---|---|
NEXT_PUBLIC_SENTRY_DSN | Sentry DSN. Sentry is disabled when this is unset. |
NEXT_PUBLIC_SENTRY_ENVIRONMENT | Sentry environment name such as development, staging, or production. |
SENTRY_AUTH_TOKEN | Optional source-map upload token for CI/Railway builds. |
SENTRY_ORG | Optional Sentry organization slug for source-map upload. |
SENTRY_PROJECT | Optional Sentry project slug for source-map upload. |
SECURITY_BOUNDARY_SENTRY_METRICS | Optional flag for security-boundary metric emission. |
environment (auto-set)
| variable | value | set by |
|---|---|---|
NODE_ENV | production | railway.toml |
PORT | 3000 | Railway (auto-injected) |
RAILWAY_ENVIRONMENT | production or Railway environment name | Railway (auto-injected) |
summary
minimum required
# Firebase Client
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=...
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...
# Firebase Admin
FIREBASE_PROJECT_ID=my-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxx@my-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
# Session
SESSION_SECRET=your-32-char-secret
MFA_ENCRYPTION_KEY=your-32-char-mfa-key
# Rate Limiting
UPSTASH_REDIS_REST_URL=https://...upstash.io
UPSTASH_REDIS_REST_TOKEN=...
# Cloudflare R2
R2_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_S3_ACCESS_KEY_ID=...
R2_S3_SECRET_ACCESS_KEY=...
# URL
NEXT_PUBLIC_BASE_URL=https://your-app.railway.app
# Cron
CRON_SECRET=your-64-char-hexfull configuration
# Firebase Client
NEXT_PUBLIC_FIREBASE_API_KEY=...
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=...
NEXT_PUBLIC_FIREBASE_PROJECT_ID=...
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=...
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=...
NEXT_PUBLIC_FIREBASE_APP_ID=...
# Firebase Admin
FIREBASE_PROJECT_ID=my-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxx@my-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n..."
# Session
SESSION_SECRET=your-32-char-secret
MFA_ENCRYPTION_KEY=your-32-char-mfa-key
# Email
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=alerts@yourdomain.com
ADMIN_EMAIL_PROD=admin@yourdomain.com
ADMIN_EMAIL_DEV=dev@yourdomain.com
SEND_WELCOME_EMAIL=true
# Rate Limiting
UPSTASH_REDIS_REST_URL=https://...upstash.io
UPSTASH_REDIS_REST_TOKEN=...
# Cloudflare R2
R2_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
R2_S3_ACCESS_KEY_ID=...
R2_S3_SECRET_ACCESS_KEY=...
ROOST_ENV=prod
# URL
NEXT_PUBLIC_BASE_URL=https://owlette.app
RAILWAY_PUBLIC_DOMAIN=owlette.app
# Cron
CRON_SECRET=your-64-char-hex
# Public Status Page
OWLETTE_STATUS_BASE_URL=https://owlette.app
INSTATUS_API_KEY=...
INSTATUS_PAGE_ID=...
INSTATUS_COMPONENT_DASHBOARD_ID=...
INSTATUS_COMPONENT_API_ID=...
INSTATUS_COMPONENT_AGENT_REGISTRY_ID=...
INSTATUS_COMPONENT_WEBHOOK_DELIVERY_ID=...
INSTATUS_COMPONENT_R2_UPLOADS_ID=...
INSTATUS_COMPONENT_FIRESTORE_ID=...
INSTATUS_COMPONENT_CORTEX_CHAT_ID=...
# LLM Encryption
LLM_ENCRYPTION_KEY=your-64-char-hex
# Autonomous Cortex (optional)
CORTEX_INTERNAL_SECRET=your-64-char-hex
# Observability (optional)
NEXT_PUBLIC_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0
NEXT_PUBLIC_SENTRY_ENVIRONMENT=production
SENTRY_AUTH_TOKEN=sntrys_...
SENTRY_ORG=your-org
SENTRY_PROJECT=owlette-websecurity notes
- Never commit
.env.localto git - Use Railway's Variables tab — values are encrypted at rest
NEXT_PUBLIC_*prefix means the value is exposed to the browser — only use for Firebase client config- Rotate secrets periodically — especially
SESSION_SECRETandCRON_SECRET - Separate environments — use different Firebase projects for dev and production