# Cumora — environment template # # Copy this file to `.env` (gitignored) and fill in real values for local # development. Required values are flagged; everything else has a sensible # default in server/src/env.ts. # # When running with `npm run server:dev` / `tsx server/...`, you can either: # - keep this as `.env` in the repo root and use a loader (dotenv-cli), # - or export these in your shell before starting the server. # ─── Core ───────────────────────────────────────────────────────────────── # Port the API + WS server listens on. Defaults to 5181. PORT=5181 NODE_ENV=development # Postgres connection string. Defaults to `postgres://@localhost:5432/cumora`. # DATABASE_URL=postgres://yetone@localhost:5432/cumora # Redis pubsub URL (used by WS bridge + agent scheduler). # REDIS_URL=redis://localhost:6379 # ─── OpenAI (required) ──────────────────────────────────────────────────── # Used for agent chat turns, classifier, and image generation. OPENAI_API_KEY=sk-... # Model overrides (optional). Cumora splits LLM calls into two tiers: # - "brain" (OPENAI_MODEL): main agent turns + convene speech. SOTA-quality # drives the product's perceived intelligence. # - "cerebellum" (OPENAI_MODEL_SUPPORT): JSON classifiers, palette, gender # inference, heartbeat agenda triage. Cheap/fast; quality matters less than # latency + cost. # - "cerebellum-summarizer" (OPENAI_COMPACTION_MODEL): auto-compaction, # turn-completion verifier, mid-turn steer summarizer. Same tier as # OPENAI_MODEL_SUPPORT by default; split out so operators can route the # long-context summarizer to a separate model (e.g. nano) if they want. # Leave blank to use the system defaults baked into env.ts. # OPENAI_MODEL=gpt-5.5 # OPENAI_MODEL_SUPPORT=gpt-5.4-mini # OPENAI_COMPACTION_MODEL=gpt-5.4-mini # OPENAI_IMAGE_MODEL=gpt-image-2 # ─── Tavily (optional) ──────────────────────────────────────────────────── # Used by the agent `web.search` tool. Without this key, web search is a no-op. # TAVILY_API_KEY= # ─── Schedulers ─────────────────────────────────────────────────────────── # Lumen background brand-audit scanner cadence (ms). Set ENABLE_SCANNER=false to skip. # SCANNER_INTERVAL_MS=90000 # Idle scheduler: each tick picks one quiet agent and inspects their Kanban # cards + current-slot Calendar events. A cerebellum classifier decides whether # anything is worth a brain wake — if yes, the brain runs with a focused brief; # if not, the tick is silently dropped (no brain call, no cost). Agents with no # agenda fall through to the original "spontaneously start a thought" behavior. # Set to 0 (or ENABLE_IDLE=false) to disable. # IDLE_INTERVAL_MS=900000 # IDLE_MIN_QUIET_MIN=25 # ─── Publicly reachable host (for OpenAI vision in dev) ────────────────── # OpenAI's input_image fetches the URL from THEIR side, so localhost won't # work. Point this at a tunnel (ngrok / cloudflared) to your :5181 if you # want vision to actually receive images uploaded in local-storage mode. # In R2 mode this is irrelevant — public URLs are already reachable. # PUBLIC_HOST=https://cumora-dev.ngrok.app # ─── Cloudflare R2 (object storage) ─────────────────────────────────────── # When ALL four core R2_ vars are set, the storage layer flips to R2 mode: # - browser uploads go directly to R2 via presigned PUT # - avatar generation persists to R2 # - server/uploads/ static-serve is disabled # Leave any of these blank and the server falls back to local disk. # Bucket endpoint. For R2 this is `https://.r2.cloudflarestorage.com`. # Find your account id at https://dash.cloudflare.com → R2 → "Use R2 with APIs". R2_ENDPOINT= # Bucket name (must already exist). R2_BUCKET= # Read+write API token credentials. Create at: # https://dash.cloudflare.com → R2 → Manage R2 API Tokens → "Create API token" # Choose "Object Read & Write" permission scoped to your bucket. R2_ACCESS_KEY_ID= R2_SECRET_ACCESS_KEY= # Public read base — your CDN / custom domain or the bucket's r2.dev URL. # When set, public URLs are stable and cacheable (`/`). # When blank, the server emits short-lived presigned GET URLs instead — # still works for the chat client, but uncacheable and rotates. # R2_PUBLIC_BASE=https://cdn.cumora.ai R2_PUBLIC_BASE= # HMAC secret shared between this server and the cumora-r2-gate Worker # (see workers/r2-gate). When set, public URLs are emitted with a signed # `?exp&sig` query string and the Worker validates them before reading R2. # Generate with `openssl rand -hex 32`. Push the SAME value to the Worker # via `cd workers/r2-gate && npx wrangler secret put R2_URL_SIGNING_SECRET`. # Leave blank to serve unsigned URLs (only OK in dev / local mode). R2_URL_SIGNING_SECRET= # TTL (seconds) baked into each signed URL. Default 1 hour. Re-signing # happens on every read, so users see fresh URLs each session. # R2_URL_TTL_SECONDS=3600 # ─── OAuth + auth post-redirect ────────────────────────────────────────── # OAuth provider creds. Leaving these blank disables the corresponding # provider on /auth/start/ (it 503s). # GOOGLE_CLIENT_ID= # GOOGLE_CLIENT_SECRET= # GITHUB_CLIENT_ID= # GITHUB_CLIENT_SECRET= # # Publicly reachable origin of THIS server — used to build the OAuth # redirect_uri handed to providers. For local dev with a tunnel, set this # to the tunnel URL. For prod, set to the public API host. # CUMORA_PUBLIC_ORIGIN=http://localhost:5181 # # Where the OAuth callback 302s the browser after creating a session, # with `#token=...&companyId=...` on the fragment. Only used when the # client didn't pass `?return=` (the browser flow now always does). In # prod, point this at the SPA host so any legacy / direct-link callback # still lands somewhere usable. # CUMORA_AUTH_DONE_URL=https://app.cumora.ai/ # # Allow-list of `?return=` URLs the OAuth start handler accepts — # without this we'd be open-redirect bait. Comma-separated; prefix match. # Must include every host the SPA may be served at (app + admin) plus # the Electron loopback page and deep-link. # CUMORA_AUTH_RETURN_ALLOWLIST=https://app.cumora.ai/,https://admin.cumora.ai/,http://localhost:5173/,http://127.0.0.1:47823/auth/done,cumora://auth # # Public base used to construct invitation accept URLs # (`/invite/`). Defaults to CUMORA_AUTH_DONE_URL — set this # only if the invite-accept flow lives on a different origin. # CUMORA_INVITE_BASE_URL=https://app.cumora.ai # # CORS allow-list. Same-origin web (SPA served from the SAME backend at # app.cumora.ai) doesn't need this. Set it when a separately-hosted web # bundle (or packaged Electron) talks to this API from another origin. # CUMORA_CORS_ORIGINS=https://app.cumora.ai # ─── Product analytics (optional) ──────────────────────────────────────── # PostHog is only initialized when VITE_PUBLIC_POSTHOG_KEY is set. These # values are baked into the Vite bundle at build time, so production Docker # and desktop release builds must receive them before `npm run build`. # VITE_PUBLIC_POSTHOG_KEY=phc_xxxxxxxx # VITE_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com # ─── SkillHub (Agent Skills registry) ──────────────────────────────────── # Base URL of an Agent Skills hub. Agents use it via # `cumora skills search ` and `cumora skills install `. # Expected contract: # GET /search?q= # → [{ name, description, version?, author?, install_url? }] # GET /skills/ (also honored: any explicit install_url) # → { name, description, version?, author?, files: [{path, body}, …] } # Leave blank to disable hub commands — agents can still create their # own skills locally with `cumora skills create`. # SKILLHUB_URL=https://skillhub.example SKILLHUB_URL= # ─── Email (real external mail per agent) ──────────────────────────────── # When all three are set, agents can send and receive real email. Outbound # goes through Resend (HTTP API). Inbound is fronted by a Cloudflare Email # Worker (workers/email-gate) that POSTs parsed JSON to # `/webhooks/email/inbound`, signed with EMAIL_INBOUND_HMAC_SECRET. # # Each agent's address is `.@`, # e.g. aurora.acme@cumora.ai. Apex domain on purpose — every tenant # shares one Resend-verified domain (DKIM/SPF set up once for EMAIL_DOMAIN # itself, no per-tenant work). # # Leave RESEND_API_KEY blank to run in mock mode — `cumora email send` # returns a fake message-id and logs instead of hitting Resend. Useful for # local dev. Inbound is gated separately on EMAIL_INBOUND_HMAC_SECRET. # # Setup: # 1. Resend: create an API key, add SPF + DKIM TXT records for EMAIL_DOMAIN. # 2. Cloudflare: enable Email Routing on EMAIL_DOMAIN, deploy the # workers/email-gate worker as the catch-all for *@. # 3. See docs/email.md for the full walkthrough. # RESEND_API_KEY=re_xxxxxxxx # EMAIL_DOMAIN=cumora.ai # EMAIL_INBOUND_HMAC_SECRET= RESEND_API_KEY= EMAIL_DOMAIN= EMAIL_INBOUND_HMAC_SECRET=