Environment Variables
If you are self-hosting Plan AI, you will need to configure environment variables for all three of the core backend applications (The Node API, the React Frontend, and the Python Voice API).
Global Configuration
Plan AI uses a master script to generate .env templates across the entire monorepo. From the root of the repository, run:
yarn setup:envBackend Variables (plan-ai/backend/.env)
This is the most critical environment file. It handles database connections, API keys, integrations, and internal service discovery.
| Variable | Description | Example |
|---|---|---|
| Core & Services | ||
ENV | The environment you are running in. | local |
PORT | The port the Express API runs on. | 8080 |
FRONTEND_URL | The public URL of the frontend app (for CORS). | http://localhost:3000 |
BACKEND_URL | The public URL of this API (for webhooks). | https://api.plan-ai.com |
APP_URL | The main URL of the app. Used to build links that leave the product — emails, and the document link written into CRM notes — so a wrong value ships a broken link to your customers. | https://plan-ai.blueberrybytes.com |
CORS_ORIGINS | Comma-separated list of allowed CORS origins. Empty in local dev allows every origin. Empty in production (NODE_ENV=production) allows only APP_URL. The desktop recorder's origin, app://recorder, is always allowed. The calendar connection only sends users back to these domains (plus APP_URL and FRONTEND_URL). | https://plan-ai.blueberrybytes.com |
API_ADMIN_KEY | Service-to-service admin key, sent in the x-admin-key header. It grants global admin, so it must be long and random: keys under 32 characters are ignored. Leave it empty to turn it off. Generate one with openssl rand -base64 48. | empty |
SECRETS_ENCRYPTION_KEY | Encrypts the stored API keys and integration tokens (AES-256-GCM). 32 bytes, base64: openssl rand -base64 32. Keep a copy outside the server: without it the stored keys cannot be read, and every workspace has to enter its keys and reconnect its integrations again. After setting it and deploying, run yarn secrets:encrypt once to encrypt the values saved before (add --dry-run to only count them). Against a remote database, run npx ts-node --transpile-only src/scripts/encryptSecrets.ts from plan-ai/backend with DATABASE_URL and the key exported: the yarn script loads the local .env, which wins over the terminal. | base64 key |
SECRETS_ENCRYPTION_KEY_PREVIOUS | Only while rotating the key: the old value of SECRETS_ENCRYPTION_KEY. Values sealed with it are still read. Run yarn secrets:encrypt to rewrite them with the new key, then remove this variable. | base64 key |
OAUTH_STATE_SECRET | Signs the OAuth state of the Google Drive, OneDrive, Linear and Notion connections. Optional; the OAuth client secret is used when empty. | base64 key |
LOG_LEVEL | Logging verbosity (info, debug, error). | info |
| Databases | ||
DATABASE_URL | PostgreSQL connection string. The password must match POSTGRES_PASSWORD when you use the compose file. | postgresql://planai:PASSWORD@localhost:5433/planai_db?schema=public |
REDIS_URL | Redis connection for BullMQ jobs. Include the password when REDIS_PASSWORD is set. | redis://:PASSWORD@localhost:6379 |
QDRANT_URL | Vector Database connection. | http://127.0.0.1:6333 |
QDRANT_API_KEY | Qdrant API key. Must match the key the Qdrant server was started with. Leave it unset only when Qdrant has no key. | random string |
QDRANT_CONTEXT_COLLECTION | The name of the collection for contexts. | context_files |
| BullMQ Dashboard | ||
BULL_BOARD_USER | Basic auth username for the queue dashboard at /admin/queues. | ops |
BULL_BOARD_PASSWORD | Basic auth password for the queue dashboard. The dashboard shows job data, which includes meeting text, so it is only mounted when both variables are set and the password has 16 or more characters. Otherwise it is off. | 16+ random characters |
| AI Providers | ||
OPENROUTER_API_KEY | System-level OpenRouter fallback key. | sk-or-v1-xxxx |
DEEPGRAM_API_KEY | System-level Deepgram fallback key. | xxxx |
STT_PROVIDER | Speech-to-text engine for the whole backend: deepgram (default) or whisper (self-hosted). See Speech-to-Text. | whisper |
WHISPER_BASE_URL | URL of the self-hosted Whisper server, without /v1. Only with STT_PROVIDER=whisper. | http://localhost:8010 |
WHISPER_MODEL | Whisper model for the post-meeting pass and voice notes. | deepdml/faster-whisper-large-v3-turbo-ct2 |
WHISPER_LIVE_MODEL | Whisper model for live captions. Defaults to WHISPER_MODEL. | Systran/faster-whisper-small |
WHISPER_LIVE_INTERIM_MS | Interim live captions every N ms; 0 turns them off. | 1500 |
WHISPER_DIARIZE | Separate speakers in the system audio when using Whisper. On when VOICE_AI_URL is set. | true |
WHISPER_DIARIZE_THRESHOLD | Cosine distance under which two utterances count as the same voice. | 0.5 |
VOICE_IDENTIFY | Name speakers after workspace members' voice profiles. On when VOICE_AI_URL is set. | true |
VOICE_IDENTIFY_MIN_SIMILARITY | Similarity needed to match a speaker with a profile. | 0.45 |
LLM_PROVIDER | openrouter (default) or local for a self-hosted OpenAI-compatible server. See Private Deployment. | local |
LOCAL_LLM_BASE_URL | Local LLM server, including /v1. | http://localhost:11434/v1 |
LOCAL_LLM_MODEL | Local model that answers every AI call. | gemma3:27b |
LOCAL_LLM_CONTEXT_TOKENS | Context window of the local model; must match the server's. | 32768 |
EMBEDDINGS_PROVIDER | openrouter (default) or local. Local vectors get their own Qdrant collection. | local |
LOCAL_EMBEDDINGS_MODEL / LOCAL_EMBEDDINGS_DIMENSION | Local embedding model and its vector size. | bge-m3 / 1024 |
OPENAI_API_KEY | Optional OpenAI key. | sk-proj-xxxx |
GROQ_API_KEY | Optional Groq key. | gsk_xxxx |
VOICE_AI_URL | Internal URL for the Python Voice API. | http://localhost:8001 |
VOICE_AI_API_KEY | Shared secret between the backend and voice-ai service. Leave empty for local dev. | your-secret-key |
| Security and privacy | ||
PLATFORM_ADMIN_SUPPORT_ACCESS | Platform admins (users with the global ADMIN role) have no access to customer workspaces. true gives them access for support, and every access is written to that workspace's audit log. Default false. | false |
SSRF_ALLOWED_HOSTS | Server-side fetches of URLs a user can influence (website import, the image proxy, the assistant's web tool, a self-hosted Jira or Twenty) cannot reach private networks. List here the internal hosts that must stay reachable, comma-separated. | jira.corp.internal,twenty.corp.internal |
SSRF_ALLOW_PRIVATE_NETWORKS | true turns the private network check off completely. Prefer SSRF_ALLOWED_HOSTS. Default false. | false |
OPENROUTER_DATA_COLLECTION | OpenRouter only routes to providers that do not collect prompts for training (deny, the default). allow removes that rule. | deny |
OPENROUTER_ZDR | true narrows OpenRouter to endpoints with zero data retention. Fewer models and providers qualify. Default false. | true |
DEEPGRAM_MIP_OPT_OUT | Deepgram requests opt out of Deepgram's Model Improvement Program by default. false opts in. Check Deepgram's pricing for the platform key before you change it. | true |
EXPOSE_API_DOCS | Swagger UI (/api-docs) and /prisma-schema are off in production unless this is true. | false |
MERMAID_INK_URL | Only the Telegram prospect flow draws diagram images on the server. By default it sends the diagram code to https://mermaid.ink. Set the URL of a self-hosted mermaid.ink, or off to skip the images. | off |
SENTRY_DSN | Backend error reports. Leave it unset to send nothing to Sentry. | https://xxxx@sentry.io/xxx |
| Integrations | ||
JIRA_CLIENT_ID | OAuth Client ID for Jira integration. | xxxx |
JIRA_CLIENT_SECRET | OAuth Client Secret for Jira integration. | xxxx |
JIRA_REDIRECT_URI | Jira OAuth callback URL. | http://localhost:8080/api/jira/callback |
TRELLO_GLOBAL_API_KEY | Trello global API key for automated OAuth. | xxxx |
GITHUB_APP_ID | GitHub App ID for codebase connection. | xxxx |
GITHUB_WEBHOOK_SECRET | Secret for GitHub webhooks. | xxxx |
GITHUB_PRIVATE_KEY | The private key for the GitHub App. | -----BEGIN RSA PRIVATE KEY-----... |
USE_GITNEXUS | Enable local GitNexus MCP indexing. | false |
GITNEXUS_MCP_URL | URL for the GitNexus MCP Server. | http://localhost:4747/api/mcp |
LINEAR_CLIENT_ID | OAuth Client ID for Linear integration. | xxxx |
LINEAR_CLIENT_SECRET | OAuth Client Secret for Linear integration. | xxxx |
LINEAR_REDIRECT_URI | Linear OAuth callback URL. | http://localhost:8080/api/linear/callback |
LINEAR_WEBHOOK_SECRET | Secret for Linear webhooks (optional). | xxxx |
GOOGLE_CLIENT_ID | Google OAuth ID (Drive integration). | xxxx |
GOOGLE_CLIENT_SECRET | Google OAuth Secret. | xxxx |
GOOGLE_REDIRECT_URI | Google OAuth callback URL. | http://localhost:8080/api/google/callback |
GOOGLE_CALENDAR_REDIRECT_URI | Google Calendar return address, a page of the web app that confirms the connection with the user's session. Leave it unset when the web runs on more than one domain: the user is sent back to the domain they started from, if it is in CORS_ORIGINS, APP_URL or FRONTEND_URL. Register /integrations/google-calendar on every domain in the OAuth client. Used as the fallback when the domain is unknown. | http://localhost:3000/integrations/google-calendar |
MICROSOFT_CLIENT_ID | Microsoft OAuth client ID (OneDrive integration). | xxxx |
MICROSOFT_CLIENT_SECRET | Microsoft OAuth client secret. | xxxx |
MICROSOFT_TENANT_ID | Microsoft Azure tenant ID. | xxxx |
MICROSOFT_REDIRECT_URI | Microsoft OAuth callback URL. | http://localhost:8080/api/microsoft/callback |
MICROSOFT_CALENDAR_REDIRECT_URI | Outlook Calendar return address, a page of the web app. Same rules as GOOGLE_CALENDAR_REDIRECT_URI: register /integrations/outlook-calendar on every web domain, under the "Web" platform of the app registration. | http://localhost:3000/integrations/outlook-calendar |
CALENDAR_STATE_SECRET | Optional. Signs the OAuth state of the calendar connections. Falls back to the Google or Microsoft client secret. | base64 key |
NOTION_CLIENT_ID | Notion OAuth client ID. | xxxx |
NOTION_CLIENT_SECRET | Notion OAuth client secret. | xxxx |
NOTION_REDIRECT_URI | Notion OAuth callback URL. | http://localhost:8080/api/notion/callback |
ASANA_CLIENT_ID | Asana OAuth client ID. | xxxx |
ASANA_CLIENT_SECRET | Asana OAuth client secret. | xxxx |
ASANA_REDIRECT_URI | Asana OAuth callback URL. | http://localhost:8080/api/asana/callback |
| Auth & Email | ||
FIREBASE_SERVICE_KEY | Base64 encoded Firebase Service Account JSON. | = |
FIREBASE_STORAGE_BUCKET | Firebase Storage bucket name. | plan-ai.appspot.com |
RESEND_API_KEY | Resend API key for transactional emails. | re_xxxx |
FROM_EMAIL | Sender address for system emails. | noreply@plan-ai.blueberrybytes.com |
| Stripe Billing (optional) — see Stripe Billing setup | ||
STRIPE_SECRET_KEY | Stripe secret key. Leave empty to disable billing entirely (OSS default). | sk_test_… |
STRIPE_WEBHOOK_SECRET | Webhook signing secret from Stripe Dashboard or CLI. | whsec_… |
STRIPE_PRICE_PRO_BYOK | Stripe Price ID for the Pro BYOK tier (€6/seat/month). | price_… |
STRIPE_PRICE_PRO_MANAGED | Stripe Price ID for the Pro Managed tier (€29/seat/month). | price_… |
STRIPE_PRICE_BUSINESS_BYOK | Stripe Price ID for the Business BYOK tier (€14/seat/month). | price_… |
STRIPE_PRICE_BUSINESS_MANAGED | Stripe Price ID for the Business Managed tier (€65/seat/month). | price_… |
STRIPE_CHECKOUT_SUCCESS_PATH | Path appended to APP_URL for Stripe success redirect. | /billing?status=success |
STRIPE_CHECKOUT_CANCEL_PATH | Path appended to APP_URL for Stripe cancel redirect. | /billing?status=canceled |
STRIPE_BYOK_TRIAL_DAYS | Free-trial length for BYOK tiers. Leave empty for no trial. | 14 |
Twenty CRM needs no variables here
Twenty is self-hosted per customer, so its instance URL and API key are stored per workspace and entered in the app under Settings → Integrations — not in this file. See CRM Sync.
Docker Compose Variables (plan-ai/backend/.env)
plan-ai/backend/docker-compose.yml reads these from the .env file next to it, which is the same file the backend uses. Every port in the compose file is published on 127.0.0.1 only. Docker writes its own firewall rules, so a port published on 0.0.0.0 would be open to the network even behind ufw.
| Variable | Description | Example |
|---|---|---|
POSTGRES_PASSWORD | Password of the planai Postgres user. Without it the compose file uses the development default planai123, so a private install must set it. DATABASE_URL must use the same password. Postgres only reads it when the data volume is first created; to change it later, change the user's password inside Postgres. | random string |
REDIS_PASSWORD | With it set, Redis requires this password. Put it in REDIS_URL too. | random string |
QDRANT_API_KEY | Qdrant has no key in the compose file by default. A private install uncomments the QDRANT__SERVICE__API_KEY line of the qdrant service; compose then refuses to start until QDRANT_API_KEY is set. The backend reads the same variable. | random string |
VOICE_AI_API_KEY | The voice service of the private stack reads it too. Set it in a private install; the backend sends the same value. | random string |
Frontend Variables (plan-ai/frontend/.env)
These variables are exposed to the React frontend application using Create React App (REACT_APP_ prefix).
| Variable | Description | Example |
|---|---|---|
PORT | Local dev port for React. | 3000 |
REACT_APP_API_BACKEND_URL | The URL of your Node Backend API. | http://localhost:8080 |
REACT_APP_ENV | Environment context for the UI. | local |
REACT_APP_ENABLE_REMOTE_LOGS | Enable/disable sending logs remotely. | false |
| Firebase Auth | ||
REACT_APP_FIREBASE_API_KEY | Public Firebase API Key. | AIzaSy... |
REACT_APP_FIREBASE_APP_ID | Public Firebase App ID. | 1:1234:web:abcd |
REACT_APP_FIREBASE_AUTH_DOMAIN | Firebase Auth Domain. | plan-ai-1234.firebaseapp.com |
REACT_APP_FIREBASE_PROJECT_ID | Firebase Project ID. | plan-ai-1234 |
REACT_APP_FIREBASE_STORAGE_BUCKET | Firebase Storage bucket. | plan-ai-1234.appspot.com |
REACT_APP_FIREBASE_MEASUREMENT_ID | Firebase Analytics ID. | G-1234 |
REACT_APP_FIREBASE_MESSAGING_SENDER_ID | Firebase Messaging ID. | 12345 |
| Google Integrations | ||
REACT_APP_GOOGLE_CLIENT_ID | For Google Drive / Docs picker. | xxxx.apps.googleusercontent.com |
REACT_APP_GOOGLE_API_KEY | For Google Drive API access. | AIzaSy... |
REACT_APP_GOOGLE_APP_ID | Google App ID. | 1234567890 |
Voice Service Variables (plan-ai/voice-ai/.env)
This powers the deep-learning speaker identification system.
| Variable | Description | Example |
|---|---|---|
PORT | The port the FastAPI service runs on. | 8001 |
HOST | The binding host. | 0.0.0.0 |
HF_HOME | Directory for caching HuggingFace models. | ./.cache/huggingface |
VOICE_AI_API_KEY | Shared secret to authenticate requests from the backend. Must match the backend value. | your-secret-key |
VOICE_REQUEST_TIMEOUT | Request timeout in seconds (default: 180). | 180 |
Desktop Recorder Variables (plan-ai-recorder/.env)
The Electron desktop application uses Vite, so all public variables are prefixed with VITE_.
| Variable | Description | Example |
|---|---|---|
VITE_PLAN_AI_API_URL | The URL of the Node Backend API. | http://localhost:8080 |
VITE_PLAN_AI_WEB_URL | The URL of the Web Dashboard. | http://localhost:3000 |
VITE_APP_PROTOCOL | The deep-linking protocol scheme. | blueberrybytes-recorder |
VITE_LOGIN_BUG | Dev flag for debugging the auth flow. | false |
| Firebase Auth | ||
VITE_FIREBASE_API_KEY | Public Firebase API Key. | AIzaSy... |
VITE_FIREBASE_AUTH_DOMAIN | Firebase Auth Domain. | plan-ai-1234.firebaseapp.com |
VITE_FIREBASE_PROJECT_ID | Firebase Project ID. | plan-ai-1234 |
VITE_FIREBASE_STORAGE_BUCKET | Firebase Storage bucket. | plan-ai-1234.appspot.com |
VITE_FIREBASE_MESSAGING_SENDER_ID | Firebase Messaging ID. | 12345 |
VITE_FIREBASE_APP_ID | Public Firebase App ID. | 1:1234:web:abcd |
VITE_FIREBASE_MEASUREMENT_ID | Firebase Analytics ID. | G-1234 |
VITE_SENTRY_DSN | Sentry DSN for error tracking. | https://xxxx@sentry.io/xxx |
Mobile App Variables (plan-ai-mobile/.env.local)
The React Native (Expo) app uses EXPO_PUBLIC_ prefixes so variables are securely bundled into the native IPA/APK.
| Variable | Description | Example |
|---|---|---|
APP_ENV | Modifies app.config.ts bundle identities. | development |
EXPO_PUBLIC_PLAN_AI_API_URL | The URL of the Node Backend API. | http://localhost:8080 or http://192.168.1.44:8080 |
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID | Google Client ID for mobile auth. | xxxx.apps.googleusercontent.com |
SENTRY_AUTH_TOKEN | Secret token for uploading source maps to Sentry. | sntrys_xxxx |
Firebase Native Configuration
In addition to the environment variables above, the native mobile app requires actual configuration files from your Firebase Console. You must download these files and place them in the root of the plan-ai-mobile directory:
google-services.json(For Android)GoogleService-Info.plist(For iOS)
⚠️ Security Warning: Do NOT commit these files to a public repository! They are included in the
.gitignoreby default. While they are embedded in your final public APK/IPA, committing them raw allows forks to accidentally use your Firebase backend for their environments.
