Skip to content

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:

bash
yarn setup:env

Backend Variables (plan-ai/backend/.env) ​

This is the most critical environment file. It handles database connections, API keys, integrations, and internal service discovery.

VariableDescriptionExample
Core & Services
ENVThe environment you are running in.local
PORTThe port the Express API runs on.8080
FRONTEND_URLThe public URL of the frontend app (for CORS).http://localhost:3000
BACKEND_URLThe public URL of this API (for webhooks).https://api.plan-ai.com
APP_URLThe 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_ORIGINSComma-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_KEYService-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_KEYEncrypts 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_PREVIOUSOnly 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_SECRETSigns the OAuth state of the Google Drive, OneDrive, Linear and Notion connections. Optional; the OAuth client secret is used when empty.base64 key
LOG_LEVELLogging verbosity (info, debug, error).info
Databases
DATABASE_URLPostgreSQL connection string. The password must match POSTGRES_PASSWORD when you use the compose file.postgresql://planai:PASSWORD@localhost:5433/planai_db?schema=public
REDIS_URLRedis connection for BullMQ jobs. Include the password when REDIS_PASSWORD is set.redis://:PASSWORD@localhost:6379
QDRANT_URLVector Database connection.http://127.0.0.1:6333
QDRANT_API_KEYQdrant 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_COLLECTIONThe name of the collection for contexts.context_files
BullMQ Dashboard
BULL_BOARD_USERBasic auth username for the queue dashboard at /admin/queues.ops
BULL_BOARD_PASSWORDBasic 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_KEYSystem-level OpenRouter fallback key.sk-or-v1-xxxx
DEEPGRAM_API_KEYSystem-level Deepgram fallback key.xxxx
STT_PROVIDERSpeech-to-text engine for the whole backend: deepgram (default) or whisper (self-hosted). See Speech-to-Text.whisper
WHISPER_BASE_URLURL of the self-hosted Whisper server, without /v1. Only with STT_PROVIDER=whisper.http://localhost:8010
WHISPER_MODELWhisper model for the post-meeting pass and voice notes.deepdml/faster-whisper-large-v3-turbo-ct2
WHISPER_LIVE_MODELWhisper model for live captions. Defaults to WHISPER_MODEL.Systran/faster-whisper-small
WHISPER_LIVE_INTERIM_MSInterim live captions every N ms; 0 turns them off.1500
WHISPER_DIARIZESeparate speakers in the system audio when using Whisper. On when VOICE_AI_URL is set.true
WHISPER_DIARIZE_THRESHOLDCosine distance under which two utterances count as the same voice.0.5
VOICE_IDENTIFYName speakers after workspace members' voice profiles. On when VOICE_AI_URL is set.true
VOICE_IDENTIFY_MIN_SIMILARITYSimilarity needed to match a speaker with a profile.0.45
LLM_PROVIDERopenrouter (default) or local for a self-hosted OpenAI-compatible server. See Private Deployment.local
LOCAL_LLM_BASE_URLLocal LLM server, including /v1.http://localhost:11434/v1
LOCAL_LLM_MODELLocal model that answers every AI call.gemma3:27b
LOCAL_LLM_CONTEXT_TOKENSContext window of the local model; must match the server's.32768
EMBEDDINGS_PROVIDERopenrouter (default) or local. Local vectors get their own Qdrant collection.local
LOCAL_EMBEDDINGS_MODEL / LOCAL_EMBEDDINGS_DIMENSIONLocal embedding model and its vector size.bge-m3 / 1024
OPENAI_API_KEYOptional OpenAI key.sk-proj-xxxx
GROQ_API_KEYOptional Groq key.gsk_xxxx
VOICE_AI_URLInternal URL for the Python Voice API.http://localhost:8001
VOICE_AI_API_KEYShared secret between the backend and voice-ai service. Leave empty for local dev.your-secret-key
Security and privacy
PLATFORM_ADMIN_SUPPORT_ACCESSPlatform 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_HOSTSServer-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_NETWORKStrue turns the private network check off completely. Prefer SSRF_ALLOWED_HOSTS. Default false.false
OPENROUTER_DATA_COLLECTIONOpenRouter only routes to providers that do not collect prompts for training (deny, the default). allow removes that rule.deny
OPENROUTER_ZDRtrue narrows OpenRouter to endpoints with zero data retention. Fewer models and providers qualify. Default false.true
DEEPGRAM_MIP_OPT_OUTDeepgram 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_DOCSSwagger UI (/api-docs) and /prisma-schema are off in production unless this is true.false
MERMAID_INK_URLOnly 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_DSNBackend error reports. Leave it unset to send nothing to Sentry.https://xxxx@sentry.io/xxx
Integrations
JIRA_CLIENT_IDOAuth Client ID for Jira integration.xxxx
JIRA_CLIENT_SECRETOAuth Client Secret for Jira integration.xxxx
JIRA_REDIRECT_URIJira OAuth callback URL.http://localhost:8080/api/jira/callback
TRELLO_GLOBAL_API_KEYTrello global API key for automated OAuth.xxxx
GITHUB_APP_IDGitHub App ID for codebase connection.xxxx
GITHUB_WEBHOOK_SECRETSecret for GitHub webhooks.xxxx
GITHUB_PRIVATE_KEYThe private key for the GitHub App.-----BEGIN RSA PRIVATE KEY-----...
USE_GITNEXUSEnable local GitNexus MCP indexing.false
GITNEXUS_MCP_URLURL for the GitNexus MCP Server.http://localhost:4747/api/mcp
LINEAR_CLIENT_IDOAuth Client ID for Linear integration.xxxx
LINEAR_CLIENT_SECRETOAuth Client Secret for Linear integration.xxxx
LINEAR_REDIRECT_URILinear OAuth callback URL.http://localhost:8080/api/linear/callback
LINEAR_WEBHOOK_SECRETSecret for Linear webhooks (optional).xxxx
GOOGLE_CLIENT_IDGoogle OAuth ID (Drive integration).xxxx
GOOGLE_CLIENT_SECRETGoogle OAuth Secret.xxxx
GOOGLE_REDIRECT_URIGoogle OAuth callback URL.http://localhost:8080/api/google/callback
GOOGLE_CALENDAR_REDIRECT_URIGoogle 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_IDMicrosoft OAuth client ID (OneDrive integration).xxxx
MICROSOFT_CLIENT_SECRETMicrosoft OAuth client secret.xxxx
MICROSOFT_TENANT_IDMicrosoft Azure tenant ID.xxxx
MICROSOFT_REDIRECT_URIMicrosoft OAuth callback URL.http://localhost:8080/api/microsoft/callback
MICROSOFT_CALENDAR_REDIRECT_URIOutlook 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_SECRETOptional. Signs the OAuth state of the calendar connections. Falls back to the Google or Microsoft client secret.base64 key
NOTION_CLIENT_IDNotion OAuth client ID.xxxx
NOTION_CLIENT_SECRETNotion OAuth client secret.xxxx
NOTION_REDIRECT_URINotion OAuth callback URL.http://localhost:8080/api/notion/callback
ASANA_CLIENT_IDAsana OAuth client ID.xxxx
ASANA_CLIENT_SECRETAsana OAuth client secret.xxxx
ASANA_REDIRECT_URIAsana OAuth callback URL.http://localhost:8080/api/asana/callback
Auth & Email
FIREBASE_SERVICE_KEYBase64 encoded Firebase Service Account JSON.=
FIREBASE_STORAGE_BUCKETFirebase Storage bucket name.plan-ai.appspot.com
RESEND_API_KEYResend API key for transactional emails.re_xxxx
FROM_EMAILSender address for system emails.noreply@plan-ai.blueberrybytes.com
Stripe Billing (optional) — see Stripe Billing setup
STRIPE_SECRET_KEYStripe secret key. Leave empty to disable billing entirely (OSS default).sk_test_…
STRIPE_WEBHOOK_SECRETWebhook signing secret from Stripe Dashboard or CLI.whsec_…
STRIPE_PRICE_PRO_BYOKStripe Price ID for the Pro BYOK tier (€6/seat/month).price_…
STRIPE_PRICE_PRO_MANAGEDStripe Price ID for the Pro Managed tier (€29/seat/month).price_…
STRIPE_PRICE_BUSINESS_BYOKStripe Price ID for the Business BYOK tier (€14/seat/month).price_…
STRIPE_PRICE_BUSINESS_MANAGEDStripe Price ID for the Business Managed tier (€65/seat/month).price_…
STRIPE_CHECKOUT_SUCCESS_PATHPath appended to APP_URL for Stripe success redirect./billing?status=success
STRIPE_CHECKOUT_CANCEL_PATHPath appended to APP_URL for Stripe cancel redirect./billing?status=canceled
STRIPE_BYOK_TRIAL_DAYSFree-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.

VariableDescriptionExample
POSTGRES_PASSWORDPassword 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_PASSWORDWith it set, Redis requires this password. Put it in REDIS_URL too.random string
QDRANT_API_KEYQdrant 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_KEYThe 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).

VariableDescriptionExample
PORTLocal dev port for React.3000
REACT_APP_API_BACKEND_URLThe URL of your Node Backend API.http://localhost:8080
REACT_APP_ENVEnvironment context for the UI.local
REACT_APP_ENABLE_REMOTE_LOGSEnable/disable sending logs remotely.false
Firebase Auth
REACT_APP_FIREBASE_API_KEYPublic Firebase API Key.AIzaSy...
REACT_APP_FIREBASE_APP_IDPublic Firebase App ID.1:1234:web:abcd
REACT_APP_FIREBASE_AUTH_DOMAINFirebase Auth Domain.plan-ai-1234.firebaseapp.com
REACT_APP_FIREBASE_PROJECT_IDFirebase Project ID.plan-ai-1234
REACT_APP_FIREBASE_STORAGE_BUCKETFirebase Storage bucket.plan-ai-1234.appspot.com
REACT_APP_FIREBASE_MEASUREMENT_IDFirebase Analytics ID.G-1234
REACT_APP_FIREBASE_MESSAGING_SENDER_IDFirebase Messaging ID.12345
Google Integrations
REACT_APP_GOOGLE_CLIENT_IDFor Google Drive / Docs picker.xxxx.apps.googleusercontent.com
REACT_APP_GOOGLE_API_KEYFor Google Drive API access.AIzaSy...
REACT_APP_GOOGLE_APP_IDGoogle App ID.1234567890

Voice Service Variables (plan-ai/voice-ai/.env) ​

This powers the deep-learning speaker identification system.

VariableDescriptionExample
PORTThe port the FastAPI service runs on.8001
HOSTThe binding host.0.0.0.0
HF_HOMEDirectory for caching HuggingFace models../.cache/huggingface
VOICE_AI_API_KEYShared secret to authenticate requests from the backend. Must match the backend value.your-secret-key
VOICE_REQUEST_TIMEOUTRequest 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_.

VariableDescriptionExample
VITE_PLAN_AI_API_URLThe URL of the Node Backend API.http://localhost:8080
VITE_PLAN_AI_WEB_URLThe URL of the Web Dashboard.http://localhost:3000
VITE_APP_PROTOCOLThe deep-linking protocol scheme.blueberrybytes-recorder
VITE_LOGIN_BUGDev flag for debugging the auth flow.false
Firebase Auth
VITE_FIREBASE_API_KEYPublic Firebase API Key.AIzaSy...
VITE_FIREBASE_AUTH_DOMAINFirebase Auth Domain.plan-ai-1234.firebaseapp.com
VITE_FIREBASE_PROJECT_IDFirebase Project ID.plan-ai-1234
VITE_FIREBASE_STORAGE_BUCKETFirebase Storage bucket.plan-ai-1234.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_IDFirebase Messaging ID.12345
VITE_FIREBASE_APP_IDPublic Firebase App ID.1:1234:web:abcd
VITE_FIREBASE_MEASUREMENT_IDFirebase Analytics ID.G-1234
VITE_SENTRY_DSNSentry 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.

VariableDescriptionExample
APP_ENVModifies app.config.ts bundle identities.development
EXPO_PUBLIC_PLAN_AI_API_URLThe URL of the Node Backend API.http://localhost:8080 or http://192.168.1.44:8080
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_IDGoogle Client ID for mobile auth.xxxx.apps.googleusercontent.com
SENTRY_AUTH_TOKENSecret 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 .gitignore by 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.