Appearance
Environment variables
Every process.env.* reference across backend/src/.
Server / runtime
| Var | Purpose |
|---|---|
PORT | HTTP listen port |
ENABLE_CRON | Master switch — whether the workspace scheduler initializes |
DISABLE_APP_EVENTS | true = the engagement ingest (/api/internal/app-events) answers 410 and browser reporters self-disable for the session |
SMOKE_TRIGGER_TOKEN | Arms 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_ENV | Environment mode |
PLATFORM_MASTER_EMAILS | Comma-separated Luniq support addresses (default [email protected]). 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
| Var | Purpose |
|---|---|
FRONTEND_URL | Allowed CORS origin; app/notification/invite link base |
CORS_ORIGINS | Comma-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_URL | Fallback app URL for notification links |
INTERNAL_DASHBOARD_URL | Internal-dashboard link in access-request alerts |
LEAD_AUDIT_ALLOWED_ORIGINS | Allowed origins for the public lead-audit router |
Email (Resend)
| Var | Purpose |
|---|---|
ENABLE_EMAILS | Master toggle for outbound email |
RESEND_API_KEY | Resend API key |
FROM_EMAIL / FROM_NAME | Sender address / display name |
REPLY_TO_EMAIL | Reply-to for invites / lead-audit emails |
ADMIN_EMAIL | Recipient for admin/access-request/support alerts |
Data / AI providers
| Var | Purpose |
|---|---|
SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY | Supabase project + service-role key |
ANTHROPIC_API_KEY | Claude API key |
OPENAI_API_KEY | Embeddings 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_KEY | Perplexity research |
DATAFORSEO_LOGIN / DATAFORSEO_PASSWORD | DataForSEO API |
UNSPLASH_ACCESS_KEY | Cover-image search |
PAGESPEED_INSIGHTS_API_KEY | Core Web Vitals (health audit) |
Billing (Stripe — the agency plan + workspaces)
Setup walkthrough: Billing: Stripe setup.
| Var | Purpose |
|---|---|
BILLING_ENABLED | true 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_KEY | Stripe API key (client constructed lazily, only when billing is on) |
STRIPE_WEBHOOK_SECRET | Signature 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_DAYS | Stripe-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_KEY | Free 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_USD | Daily spend ceiling for the public docs assistant (/api/agent/docs-chat); default 10 |
PRODUCT_DOCS_DIR | Override path for the agent's product-docs snapshot (normally the committed modules/agent/product-docs/) |
Public pages
| Var | Purpose |
|---|---|
PUBLIC_APP_BASE | Fallback 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
| Var | Purpose |
|---|---|
GOOGLE_SERVICE_ACCOUNT_JSON | Shared GSC service-account credentials (inline JSON) |
GOOGLE_SERVICE_ACCOUNT_FILE | Path 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
| Var | Purpose |
|---|---|
LEAD_AUDIT_ENABLED | Toggle the public lead-audit feature |
LEAD_AUDIT_DAILY_CAP | Daily cap on inbound audit runs (outreach batch excluded) |
LEAD_AUDIT_PUBLIC_BASE | Public origin for batch-status report links (defaults to the request host) |
LEAD_AUDIT_OUTREACH_KEY | API key (x-api-key) for the CRM batch endpoints; unset = key auth off, internal sessions only |
TURNSTILE_SECRET_KEY | Cloudflare Turnstile captcha secret |