# Postgres DATABASE_URL="postgresql://postgres:postgres@localhost:5432/crm?schema=public" # A direct, unpooled connection to that same database, used by # `prisma migrate deploy` when the API is built on Vercel. Set it only if # DATABASE_URL points at a pooler; POSTGRES_URL_NON_POOLING is read as a # fallback. Locally there is nothing to set. # DIRECT_DATABASE_URL="" # A throwaway database for `bun run test`. The suite will not touch DATABASE_URL # and refuses to run without this — these are real integration tests, they write # and delete rows, and the pre-push hook runs them. The name has to end in # `_test`. `bun run db:test` creates it and applies the migrations. TEST_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/crm_test?schema=public" # Generate your own: openssl rand -base64 32 BETTER_AUTH_SECRET="" # Who is allowed to sign in — this is the entire authorisation model, so it has # no default. Comma-separated, and each entry is either a whole email domain or # a single address: # # ALLOWED_SIGN_IN="acme.com" everyone at your company # ALLOWED_SIGN_IN="acme.com,contractor@gmail.com" …plus one outsider # ALLOWED_SIGN_IN="you@gmail.com" a one-person install # # Subdomains count, so "acme.com" also admits "you@mail.acme.com". ALLOWED_SIGN_IN="" # Google is the sign-in method a clone starts with, and the same client is what # reads Gmail and Calendar. Set both or neither — half a pair is a sign-in # button that fails at Google. # # Leave them empty only if you sign in with Microsoft below, or with your own # identity provider added on Settings → SSO. With none of the three there is no # way in at all, and the sign-in page says as much rather than showing you # nothing. GOOGLE_CLIENT_ID="" GOOGLE_CLIENT_SECRET="" # Microsoft 365 / Entra ID — the other sign-in method, and the same app # registration is what reads Outlook mail. Set both or neither, exactly like # the Google pair. # # Create the app at https://portal.azure.com → Microsoft Entra ID → App # registrations, add the redirect URI /api/auth/callback/microsoft, # and give it the delegated Graph permissions User.Read and Mail.Read. The # README has the full walkthrough. # # You can set Google and Microsoft together: the sign-in page offers both, and # a rep's mail is read from whichever they signed in with. # MICROSOFT_CLIENT_ID="" # MICROSOFT_CLIENT_SECRET="" # Optional. Enables Slack account linking on Settings > Connections. # Add APP_URL + /api/auth/oauth2/callback/slack as the Slack OAuth redirect URL. # SLACK_CLIENT_ID="" # SLACK_CLIENT_SECRET="" # Which Entra tenant may sign in. "common" (the default) accepts any work, # school or personal Microsoft account and leans on ALLOWED_SIGN_IN to decide # who actually gets in; your own tenant's GUID refuses everyone else at # Microsoft, before they ever reach us. "organizations" allows any work or # school account but no personal ones. # MICROSOFT_TENANT_ID="common" # Serve the landing page at "/". It markets *this* product, so it is off unless # you say otherwise: on an install of your own, a stranger arriving at the root # is sent to /sign-in instead. The only value that turns it on is "true". # IS_MARKETING="true" # ── Where things are ───────────────────────────────────────────────────────── # Only needed when you deploy. The defaults below are the localhost ones. # The API. Also the origin that mints session cookies and serves /api/auth/*. # API_URL="http://localhost:3001" # The web app. Comma-separate if it is served from more than one origin — # the first is canonical, and the rest are simply allowed to call the API. # APP_URL="http://localhost:3000" # Only when the API and the app are on different subdomains of one parent, # e.g. api.example.com and app.example.com — then set ".example.com" so one # session cookie covers both. # AUTH_COOKIE_DOMAIN="" # The research agent, which is its own deployment. The API reads this too, to # tell the agent a logo or a photograph is waiting rather than letting it find # out on its next minute. # AGENT_URL="http://127.0.0.1:2000" # Port used by the self-hosted Eve service. Hosted runtimes can keep injecting # their standard PORT variable instead. # AGENT_PORT="2000" # Lets a signed-in rep talk to the agent from the contact sheet. # # The browser never calls the agent directly. The app proxies /eve/v1/* on its # own origin, checks the session, and mints a two-minute token signed with this # secret; the agent verifies it and learns *which rep* is asking. Set the same # value for both processes. openssl rand -base64 32 # # It also authorises two calls the API makes to the agent: the dispatch poke, # which is what makes a new company's logo appear as it is added instead of on # the next minute's tick, and the check that a Context API key is real before # it is saved. # # Leave it unset and the Agent tab reports that it is not configured, the poke # is skipped rather than sent unauthenticated, and a Context key is saved # without being checked. Nothing else changes: the agent still runs on its own # schedule. # AGENT_BRIDGE_SECRET="" # PORT="3001" # ── Optional: what the agent can do ────────────────────────────────────────── # The research agent works with none of these — it falls back to what the CRM # already knows (your own email and calendar history) and simply reports what # it could not check. Each key you add unlocks one more place it can look, and # it tells you at startup which ones are on. # Perplexity — finds where a person lives on the web, and recent news worth # knowing before a call. https://perplexity.ai/settings/api # PERPLEXITY_API_KEY="" # GitHub — raises the rate limit when matching contacts to GitHub profiles. # Any classic token with no scopes will do. # GITHUB_TOKEN="" # Vercel Blob — where every logo and profile picture in the CRM is kept. # # The URLs these arrive on are somebody else's: LinkedIn signs theirs with an # expiry a few weeks out, a brand CDN is a rate limit on a page that draws forty # logos, and a company's own favicon is served by a company with no idea we are # drawing it. So the bytes are copied once and the record points at our copy. # # Read by the agent, by the API — which writes a favicon and a signed-in user's # Google avatar — and by the seed. Not by the Next.js app, which only needs to # recognise one of our URLs to route it through the image optimizer. # # Without it, a contact's photograph is not stored at all, because a URL that # works today and 404s next month is worse than initials and nobody can tell # which it will be. Everything else — logos, favicons, avatars — keeps the # origin's URL and renders as an ordinary hotlinked image. # BLOB_READ_WRITE_TOKEN="" # The model the agent runs on is reached through the Vercel AI Gateway. On # Vercel this is handled by OIDC and needs nothing; elsewhere, set a key. # Which model is a setting, not a variable — change it on the settings page. # https://vercel.com/docs/ai-gateway # AI_GATEWAY_API_KEY="" # ── Optional: operations ───────────────────────────────────────────────────── # Shared cache. Without it each instance caches in its own memory, which is # correct — just not shared. Recommended in production once website tracking is # on: the compiled tracking config, the collector's rate limit and the hourly # cap on contacts created from forms are all counted here, and per-instance # counters let a multi-instance deployment exceed both. # REDIS_URL="redis://localhost:6379" # CACHE_TTL_MS="60000" # Bearer token guarding POST /internal/sync/google, the Gmail/Calendar cron # route, and POST /internal/tracking/retention, the nightly sweep that deletes # tracked page views older than 90 days. Both refuse to run without it. At # least 16 characters. # CRON_SECRET="" # Log every SQL statement Prisma runs, at debug level, without bound # parameters. Off by default: it buries every other line under a wall of # SELECTs. # PRISMA_LOG_QUERIES="true" # ── Anonymous usage telemetry ──────────────────────────────────────────────── # This repo is MIT and cloned rather than installed, so there is no download # count and no runtime signal. Once a day the API sends one event of counts — # how many contacts exist in bands rather than exactly, which of the agent's # tools were called, how the evidence ledger behaved, which optional keys are # set as booleans — tied to a random UUID generated by the first migration. # # It is server-side only. There is no posthog-js anywhere, no autocapture and # no session replay, because autocapture on a CRM would lift contact names and # deal amounts out of your database. No name, address, company, subject line, # amount, prompt or key is ever sent, and neither is your IP: every event # carries $ip: null with geoip off. # # docs/telemetry.md names every event and every property that is sent, and # lists what never is. # Turn it off. "1" is the whole interface, and DO_NOT_TRACK=1 does the same. # When either is set no client is created, so nothing can be sent later. # # There is no variable for where it goes: the project key and host are written # into packages/telemetry, because a write-only key is not a secret and a # variable would only suggest it were one. Change them there if you want the # numbers in your own project. # CRM_TELEMETRY_DISABLED="1"