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.localfor local dev; set the same keys in Vercel's project settings for production)..env.docker.example- for the self-hosted Docker stack (copy to.envnext todocker-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_URLstringRequiredVercelYour 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_KEYstringRequiredVercelThe 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_KEYstringOptionalThe 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_SECRETstringRequiredAuthenticates 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_URLstringOptionalOverrides 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_PASSWORDstringOptionalOverrides 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_JSONstringOptionalThe 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_URLstringOptionalYour 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_IDstringOptionalClient 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_SECRETstringOptionalThe matching secret from the same OAuth client in Google Cloud Console.
OAUTH_STATE_SECRETstringOptionalAny 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_LOGINstringOptionalYour DataForSEO account email.
DATAFORSEO_PASSWORDstringOptionalThe 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_KEYstringOptionalEncrypts 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_KEYWORDSstringOptionalComma-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_TOKENstringOptionalA 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_REPOstringOptionalAn 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_KEYstringOptionalA free API key from resend.com/api-keys - no credit card, no domain setup required to get started.
ALERT_EMAILstringOptionalWhere 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_FROMstringOptionalLeave 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 onlyThe 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 onlyThe 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 onlyPuts 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 onlyForces 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 onlyYour 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 onlyOverrides 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 onlyHow 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 onlyPassword 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 onlySwitches 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 onlyServes 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 onlyThe 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 onlyBilling access token from polar.sh → Settings → API.
POLAR_WEBHOOK_SECRETstringOptionalCloud onlyThe 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 onlySet to sandbox to switch to Polar's test environment. Leave unset for the
live one.
POLAR_PRODUCT_STARTERstringOptionalCloud onlyThe Polar product id for the Starter tier.
POLAR_PRODUCT_GROWTHstringOptionalCloud onlyThe Polar product id for the Growth tier.
POLAR_PRODUCT_SCALEstringOptionalCloud onlyThe Polar product id for the Scale tier.
POLAR_FOUNDING_DISCOUNT_IDstringOptionalCloud onlyThe 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 onlyThe 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 onlyThe App's URL name (github.com/apps/<slug>) - if unset, the install link
404s.
GITHUB_APP_PRIVATE_KEYstringOptionalCloud onlyThe 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 onlyThe secret you set when pointing the App's webhook at
https://<domain>/api/github/webhook.
GITHUB_APP_CLIENT_IDstringOptionalCloud onlyRequired 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 onlyThe matching secret for GITHUB_APP_CLIENT_ID above.
GITHUB_APP_REQUIRE_INSTALL_PROOF1OptionalCloud onlySet 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 onlyYour 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 onlyThe matching API password for DATAFORSEO_PLATFORM_LOGIN, from the same
app.dataforseo.com/api-access page
described above.
Analytics
NEXT_PUBLIC_POSTHOG_PROJECT_TOKENstringOptionalProject 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_HOSTstringOptionalhttps://us.i.posthog.com for US Cloud, https://eu.i.posthog.com for EU
Cloud - match whichever region your PostHog project is on.
DispatchSEO