Skip to content

Environment variables

Every process.env.* reference across backend/src/.

Server / runtime

VarPurpose
PORTHTTP listen port
ENABLE_CRONMaster switch — whether the workspace scheduler initializes
DISABLE_APP_EVENTStrue = the engagement ingest (/api/internal/app-events) answers 410 and browser reporters self-disable for the session
SMOKE_TRIGGER_TOKENArms POST /api/internal/smoke (the read-only live-smoke trigger the Staging smoke CI job calls; unset = the route answers 404). Same value lives as the repo's one Actions secret — see deploy.md § Secrets
NODE_ENVEnvironment mode
PLATFORM_MASTER_EMAILSComma-separated Luniq support addresses (default info@luniq.io). Read in ONE place — core/services/support-identity.js — by both the access rail (workspace/agency/masters.js, which fans a workspace_members row onto every workspace) and the exclusion every product surface applies (withoutSupport): support is on every workspace so it can reach one, so it is never listed as a person, offered as an assignee, described to the agent as a colleague, or emailed a notification. Changing it moves both halves together

CORS / URLs

VarPurpose
FRONTEND_URLAllowed CORS origin; app/notification/invite link base
CORS_ORIGINSComma-separated additional allowed origins. Outside Railway (no RAILWAY_ENVIRONMENT_NAME) any localhost/127.0.0.1 origin is trusted automatically (2026-09-02, api/allowed-origins.js) — dev port slots need no enumeration; deployed backends still use only the explicit list
APP_URLFallback app URL for notification links
INTERNAL_DASHBOARD_URLInternal-dashboard link in access-request alerts
LEAD_AUDIT_ALLOWED_ORIGINSAllowed origins for the public lead-audit router

Email (Resend)

VarPurpose
ENABLE_EMAILSMaster toggle for outbound email
RESEND_API_KEYResend API key
FROM_EMAIL / FROM_NAMESender address / display name
REPLY_TO_EMAILReply-to for invites / lead-audit emails
ADMIN_EMAILRecipient for admin/access-request/support alerts

Data / AI providers

VarPurpose
SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEYSupabase project + service-role key
ANTHROPIC_API_KEYClaude API key
OPENAI_API_KEYEmbeddings for site-context; also chat-composer dictation (/v1/audio/transcriptions). Two guarded doors, one key — absent, dictation returns 503 and the mic hides itself
PERPLEXITY_API_KEYPerplexity research
DATAFORSEO_LOGIN / DATAFORSEO_PASSWORDDataForSEO API
UNSPLASH_ACCESS_KEYCover-image search
PAGESPEED_INSIGHTS_API_KEYCore Web Vitals (health audit)

Billing (Stripe — the agency plan + workspaces)

Setup walkthrough: Billing: Stripe setup.

VarPurpose
BILLING_ENABLEDtrue turns the Stripe layer on (core/stripe/): the webhook answers, the wizard's last step offers Checkout to any agency without a plan in force, and billingAccess gates — an agency with neither an active/trialing subscription nor an assigned plan stops (create gate payment_required, isWorkspaceActive false → no crons, no billable routes). Off = nothing gated; everything runs on what Luniq assigns. Apply db/agency-plan-migration.sql and assign a plan to every agency you keep BEFORE flipping it
STRIPE_SECRET_KEYStripe API key (client constructed lazily, only when billing is on)
STRIPE_WEBHOOK_SECRETSignature secret for POST /stripe/webhook (raw-body, pre-JSON mount)
(no Price ids)Prices are resolved by lookup key<plan>-agency / <plan>-workspace for every plan in PRICING_PLANS (core/services/account-billing.js), e.g. early-bird-agency, early-bird-workspace. The mirror reads the same keys off subscription items, so a Price without a lookup key mirrors nothing. Adding a plan = a catalog entry + two Prices, no env
STRIPE_TRIAL_DAYSStripe-side trial on new Checkouts (card up front). Default 0 = none. Card-free trials are the assigned plan with an end date (internal dashboard)
MAXMIND_LICENSE_KEYFree MaxMind licence key. postinstall (scripts/update-geoip.mjs) uses it to refresh the pixel's GeoLite2 snapshot, so every deploy geolocates against current data. Unset = skip, exit 0 — a missing key must never break npm install or a deploy; the staleness surfaces as bad data instead, via check:data's geo.database-freshness. Left unset, the bundled snapshot silently ages (it was 995 days old during the 2026-07-12 geo incident — see Statistics § Geo)
DOCS_CHAT_DAILY_CAP_USDDaily spend ceiling for the public docs assistant (/api/agent/docs-chat); default 10
PRODUCT_DOCS_DIROverride path for the agent's product-docs snapshot (normally the committed modules/agent/product-docs/)

Public pages

VarPurpose
PUBLIC_APP_BASEFallback origin for share links (<base>/s/:token). Normally unset: a link points at the app that minted it, resolved from the request's allowlisted Origin (api/allowed-origins.js). Default https://orbit.luniq.io

Google Search Console

VarPurpose
GOOGLE_SERVICE_ACCOUNT_JSONShared GSC service-account credentials (inline JSON)
GOOGLE_SERVICE_ACCOUNT_FILEPath alternative to the inline JSON

The property is per-workspace (settings.gscSiteUrl, written by the connection test) and resolved everywhere through resolveGscSite. There is no global property env var: one existed (SEARCH_CONSOLE_SITE_URL, read only by the dashboard) and was removed — it let the dashboard target a different property than the sync, and pointed the whole fleet at one property.

Lead audit

VarPurpose
LEAD_AUDIT_ENABLEDToggle the public lead-audit feature
LEAD_AUDIT_DAILY_CAPDaily cap on inbound audit runs (outreach batch excluded)
LEAD_AUDIT_PUBLIC_BASEPublic origin for batch-status report links (defaults to the request host)
LEAD_AUDIT_OUTREACH_KEYAPI key (x-api-key) for the CRM batch endpoints; unset = key auth off, internal sessions only
TURNSTILE_SECRET_KEYCloudflare Turnstile captcha secret