DocsEnvironment variables

Environment variables

Every variable DispatchSEO reads, what it does, and where to get the value.

On this page

Most installs set almost nothing

The setup wizard generates MCP_API_KEY, CRON_SECRET, and your dashboard password the moment you first open the app, and stores all three in the database. Nearly everything below is an override, not a requirement - you only set an env var when you want to force a specific value, or when a feature (Search Console, DataForSEO, email alerts) needs a credential the wizard has no way to generate for you. Self-hosted Docker installs need exactly one required variable, CRON_SECRET, and start.sh writes that for you automatically.

There are two example files, matching the two ways to run DispatchSEO:

  • .env.local.example - for a Vercel deploy (copy to .env.local for local dev; set the same keys in Vercel's project settings for production).
  • .env.docker.example - for the self-hosted Docker stack (copy to .env next to docker-compose.yml).

Fields below are tagged Cloud only or Docker only where a variable is meaningless outside that mode. An untagged field applies to both a self-hosted Vercel deploy and DispatchSEO's own cloud.

Database

Self-hosted Docker skips this whole section - it bundles its own Postgres and PostgREST (see Install on your computer) and never talks to Supabase. This section matters if you deploy your own instance to Vercel, or if you're looking at what backs the hosted dispatchseo.com.

SUPABASE_URLstringRequiredVercel

Your Supabase project's URL, e.g. https://your-project-ref.supabase.co. Create a free project at supabase.com, then apply the migrations in supabase/migrations/. Find the URL on the project's Settings → API page.

SUPABASE_SERVICE_ROLE_KEYstringRequiredVercel

The service_role secret key from the same Settings → API page - not the anon key. It bypasses row-level security, which is intentional: every caller in this codebase is trusted server code (MCP tools and crons), and there is no end-user auth model to enforce.

The Vercel Marketplace's Supabase integration injects this same value under a different name, SUPABASE_SECRET_KEY, when you connect a project through it instead of pasting the key yourself. Both names work - the code checks SUPABASE_SERVICE_ROLE_KEY first and falls back to SUPABASE_SECRET_KEY, so you never need to rename anything either integration gives you.

Authentication and secrets

MCP_API_KEYstringOptional

The bearer token an external Claude Code agent uses to authenticate to your /api/mcp endpoint. Fresh deploys generate this at /setup and store it in the database - most installs never set this env var. Set it yourself (openssl rand -hex 24) only to force a specific value, which overrides whatever is stored; that is how a pre-wizard install keeps working.

CRON_SECRETstringRequired

Authenticates scheduled requests to every /api/cron/* route. Self-hosted Docker: required - start.sh generates one into .env for you, or you can set your own with openssl rand -hex 24; the app container refuses to start without it. Vercel/cloud: same wizard-generated-and-stored pattern as MCP_API_KEY above, so you rarely need to touch it - except that Vercel's own cron scheduler and any GitHub Actions workflow you run against your instance can only send a value they were explicitly given, not read your database. If you want those higher-frequency schedules to authenticate against a self-hosted Vercel deploy, set this as a real env var and use the same value as the matching GitHub Actions secret.

APP_URLstringOptional

Overrides this deploy's own URL. Normally you never set this: the setup wizard captures the deploy's own URL at claim time and every connected repo's pipeline uses that automatically (falling back to Vercel's VERCEL_PROJECT_PRODUCTION_URL for installs claimed before that column existed); on Docker, start.sh fills it in and the in-stack builder never even needs it to be publicly reachable. Set it only to force a specific domain - for example, you front the deploy with a custom domain the claim request never saw.

DASHBOARD_PASSWORDstringOptional

Overrides the dashboard's login gate. Fresh deploys choose this at /setup and store a hash of it; setting the env var overrides the stored one, which is how pre-wizard installs keep working. If you do set it, make it strong - on a self-hosted, non-cloud install it is the only thing standing between the internet and your data.

Google Search Console

GSC_SERVICE_ACCOUNT_JSONstringOptional

The full service-account JSON key, pasted as a single line. Create one in Google Cloud Console (APIs & Services → Credentials → Create service account → Keys → Add key → JSON), then add that service account's email as a Restricted user on your Search Console property - the setup wizard walks you through exactly this step. Free; no paid Google API involved.

GSC_SITE_URLstringOptional

Your Search Console property identifier, in the exact format Search Console uses: sc-domain:example.com for a domain property, or https://example.com/ (trailing slash required) for a URL-prefix property. Getting the format wrong is the most common cause of "no GSC data" - see Troubleshooting.

GOOGLE_OAUTH_CLIENT_IDstringOptional

Client ID for the dashboard's "Connect Search Console" button - an alternative to pasting a service-account JSON, mainly relevant to cloud and demo installs. Self-hosters normally use the service-account path above instead. Create an OAuth client (Web application type) in Google Cloud Console, with redirect URI https://<your-domain>/api/oauth/google/callback.

GOOGLE_OAUTH_CLIENT_SECRETstringOptional

The matching secret from the same OAuth client in Google Cloud Console.

OAUTH_STATE_SECRETstringOptional

Any long random string (openssl rand -hex 32 works) - signs the CSRF state for the Google OAuth flow above. Cloud deploys set this so DASHBOARD_PASSWORD can be removed entirely in favor of real accounts; self-hosters can leave it empty, since the dashboard password already doubles as the signing key in that mode.

Keyword data (DataForSEO)

Leaving all of these empty runs DispatchSEO in GSC-only free mode - paid features skip gracefully instead of failing. See the data tiers table in Day to day for what each tier actually unlocks.

DATAFORSEO_LOGINstringOptional

Your DataForSEO account email.

DATAFORSEO_PASSWORDstringOptional

The auto-generated API password from app.dataforseo.com/api-access - this is not your dashboard login password, and pasting the login password here is the single most common DataForSEO setup mistake. Only the default project falls back to this pair from the environment; every other project brings its own DataForSEO account, since each project's calls bill its own owner.

DATAFORSEO_ENC_KEYstringOptional

Encrypts per-project DataForSEO API passwords at rest (AES-256-GCM). Generate with openssl rand -base64 32, and use the same value in local dev and in Vercel - rotating it makes already-stored passwords undecryptable. Not needed in the GitHub Actions pipeline your site's repo runs; those workflows carry their own DATAFORSEO_* secrets directly.

Since migration 0027 this is fully optional: a claimed instance auto-generates a key into instance_settings.enc_key, and the env var only overrides it.

Setting this after credentials are saved breaks them

If you set DATAFORSEO_ENC_KEY after a project has already saved DataForSEO credentials under the auto-generated key, those credentials become permanently undecryptable - they will show as "not connected" on the dashboard with no error explaining why. If this happens to you, reconnect the credentials on the dashboard after switching keys; there is no way to recover the old ciphertext.

SEED_KEYWORDSstringOptional

Comma-separated topics around your product (e.g. time tracking,invoicing for freelancers) that give the weekly research run extra starting points. Optional - research already seeds itself from your tracked keywords and your top Search Console queries; use this to nudge it toward topics you don't rank for yet.

GitHub

GH_MERGE_TOKENstringOptional

A GitHub personal access token (repo scope) that lets the dashboard's merge and re-run-validation buttons act directly, instead of only linking out to the PR or Action on GitHub for you to click through. Without it those buttons still show - they just open GitHub instead of firing in one tap. Create one at github.com/settings/tokens.

SEO_TARGET_REPOstringOptional

An owner/repo string, e.g. you/your-site. This one isn't in either example file - it's read directly in src/lib/projects.ts, and only by the synthetic "env fallback" project that the app builds in memory on the rare occasion its projects table query fails (usually because migration 0004 hasn't been applied yet). Normal project resolution never touches it. Set it only if you've hit that specific fallback and want it to point at the right repo instead of nothing; nearly every install can ignore this variable completely.

For cloud's one-click "Connect GitHub" install flow, see the GITHUB_APP_* variables under Cloud-only settings below - that's a separate integration from the personal-access-token path here.

Email alerts

Optional, but strongly recommended once you're running in automatic publish mode - see Get emailed when something breaks for the full two-minute walkthrough.

RESEND_API_KEYstringOptional

A free API key from resend.com/api-keys - no credit card, no domain setup required to get started.

ALERT_EMAILstringOptional

Where failure emails go - at most one per job per 24 hours. Must be the same email you signed up to Resend with, unless you've verified your own domain there (see ALERT_EMAIL_FROM below).

ALERT_EMAIL_FROMstringOptional

Leave empty to send from Resend's shared test sender. Once you verify your own domain in Resend, set this to something like DispatchSEO <alerts@your-domain.com> to send from it, and to any address rather than only your own.

Docker-only settings

None of these apply to a Vercel deploy - they configure the bundled container stack described in Install on your computer.

DISPATCH_PORTnumberOptionalDocker only

The host port the dashboard is published on (the container always listens on 3000 internally). Leave unset and start.sh picks 4005, or the next free port up if something already holds it. Set it to pin a specific port.

DISPATCH_BINDstringOptionalDocker only

The host interface the dashboard is published on. Defaults to 127.0.0.1 - reachable from the machine running Docker and nowhere else, which is what you want, since it holds your GitHub and Claude tokens. Only set this to 0.0.0.0 if you deliberately want it reachable on your LAN and you have a firewall in front of it - Docker's published ports bypass tools like ufw, so "I enabled my firewall" isn't the protection it sounds like.

DOMAINstringOptionalDocker only

Puts the dashboard on your own domain with automatic HTTPS. Point the domain's DNS A record at the machine, set this, and re-run start.sh - the bundled Caddy proxy (the domain compose profile) handles certificates and renewal on its own. Leave unset if you run your own reverse proxy instead (then set APP_URL above).

BUILD_FROM_SOURCE1OptionalDocker only

Forces building the app and builder images from your own clone's source instead of pulling the prebuilt ones from GHCR. Set this if you forked the repo and changed code - otherwise start.sh prefers the prebuilt images when they're pullable, and your local changes would silently not run.

CLAUDE_CODE_OAUTH_TOKENstringOptionalDocker only

Your Claude Code OAuth token - lets the in-stack builder container run your agent headlessly, on your own existing Claude subscription. Get one by running claude setup-token on your own machine and copying the sk-ant-oat... token it prints, with no line breaks. Easiest path: skip this entirely and paste the token on the dashboard's "Turn on automatic builds" step instead - it's stored encrypted and the builder picks it up within a few minutes, no file to edit. Setting it here still works and overrides the dashboard-stored value, which is handy for scripted or headless installs. Without either, the builder idles and tells you so in its logs.

BUILDER_GH_TOKENstringOptionalDocker only

Overrides the GitHub token the builder uses to clone, push, and open PRs. Normally not needed - the builder reuses the token you connected in the setup wizard's "One-tap merge" step.

BUILDER_POLL_SECONDSnumberOptionalDocker only

How often the builder asks the backend whether anything is due, in seconds. Default 600 (10 minutes). Lower it if you want a build to start sooner after you approve something; the backend's own suggested cadence takes over once this is unset.

POSTGRES_PASSWORDstringOptionalDocker only

Password for the bundled Postgres container. Only takes effect before the first boot - it's baked into the dispatch-pgdata volume at creation, and changing it afterward requires wiping that volume (docker compose down -v, which deletes all data). The default is fine for most setups: Postgres is reachable only inside the compose network, never from the host or the internet.

Cloud-only settings

Everything in this section only matters for CLOUD_MODE=true deploys - the hosted dispatchseo.com and anyone running their own multi-tenant instance of it. Self-hosters running a single site leave all of this unset.

CLOUD_MODEtrueOptionalCloud only

Switches the dashboard from a single self-host password to real multi-user accounts: Supabase Auth sign-in, per-user project ownership (migration 0031), and billing. Self-hosters leave this unset.

LANDING_ENABLEDtrueOptionalCloud only

Serves the marketing landing page and cloud waitlist at / instead of going straight to the dashboard login. Self-hosted installs leave this unset.

SUPABASE_ANON_KEYstringOptionalCloud only

The same Supabase project's anon/publishable key, from Settings → API. Auth sessions run through this; server code keeps using the service-role key from the Database section above.

POLAR_ACCESS_TOKENstringOptionalCloud only

Billing access token from polar.sh → Settings → API.

POLAR_WEBHOOK_SECRETstringOptionalCloud only

The secret from the webhook endpoint you create in Polar, pointed at https://<domain>/api/polar/webhook, subscribed to the customer.state_changed event.

POLAR_SERVERstringOptionalCloud only

Set to sandbox to switch to Polar's test environment. Leave unset for the live one.

POLAR_PRODUCT_STARTERstringOptionalCloud only

The Polar product id for the Starter tier.

POLAR_PRODUCT_GROWTHstringOptionalCloud only

The Polar product id for the Growth tier.

POLAR_PRODUCT_SCALEstringOptionalCloud only

The Polar product id for the Scale tier.

POLAR_FOUNDING_DISCOUNT_IDstringOptionalCloud only

The Polar discount id for the "50% off, locked for life" founding offer advertised on the pricing page. In the Polar dashboard: Discounts → New, type Percentage, 50%, duration Forever (not "Once" or "Repeating" - the promise is for life), applied to all three products, no redemption limit. Leave unset and checkout charges full list price with no discount applied.

GITHUB_APP_IDstringOptionalCloud only

The App ID from your GitHub App's settings page, once you've created one for the one-click "Connect GitHub" install flow.

GITHUB_APP_SLUGstringOptionalCloud only

The App's URL name (github.com/apps/<slug>) - if unset, the install link 404s.

GITHUB_APP_PRIVATE_KEYstringOptionalCloud only

The full PEM private key from the App's settings page, newlines and all - paste it whole. The code tolerates Vercel's \n-escaping of multi-line env values.

GITHUB_APP_WEBHOOK_SECRETstringOptionalCloud only

The secret you set when pointing the App's webhook at https://<domain>/api/github/webhook.

GITHUB_APP_CLIENT_IDstringOptionalCloud only

Required alongside the four GITHUB_APP_* variables above. The App must also have "Request user authorization (OAuth) during installation" enabled, with its Callback URL set to <origin>/api/github/install/callback. This proves the person who clicked Install actually controls the installation being claimed; without it, claim verification falls back to a weaker freshness heuristic, and the deploy check alarms in CLOUD_MODE if it's missing.

GITHUB_APP_CLIENT_SECRETstringOptionalCloud only

The matching secret for GITHUB_APP_CLIENT_ID above.

GITHUB_APP_REQUIRE_INSTALL_PROOF1OptionalCloud only

Set to 1 only after you've watched a real install verify successfully. A callback that carries a ?code and fails verification is always refused regardless of this flag - it only controls the ambiguous case of a callback arriving with no code at all, which usually means the App isn't actually configured for user authorization. Left unset, that case falls back to the freshness/suspension gate instead of a hard refusal, which still closes every orphan-installation class; turning this on before confirming the App is configured correctly risks blocking honest customers.

DATAFORSEO_PLATFORM_LOGINstringOptionalCloud only

Your own bundled DataForSEO account that bills every paid project which never connected its own credentials. Per-tier budgets (migration 0035) cap the spend.

DATAFORSEO_PLATFORM_PASSWORDstringOptionalCloud only

The matching API password for DATAFORSEO_PLATFORM_LOGIN, from the same app.dataforseo.com/api-access page described above.

Analytics

NEXT_PUBLIC_POSTHOG_PROJECT_TOKENstringOptional

Project API token from your PostHog project settings. Self-hosters can point this at their own PostHog project, or leave it empty to run with none - both the client and server instrumentation no-op without a token.

NEXT_PUBLIC_POSTHOG_HOSTstringOptional

https://us.i.posthog.com for US Cloud, https://eu.i.posthog.com for EU Cloud - match whichever region your PostHog project is on.