Configuration reference
Every environment variable Sphericon reads, with its default. You do not need most of them: a working Instance needs only the four required ones and a mail provider (set up in the app, see the Quickstart). The rest switch on specific features.
Not installed yet? Start with Self-hosting.
Configuration is read from environment variables (and, if present, .env files next to
the binary). SPHERICON_ENV selects the environment (development by default; set it to
production when self-hosting — the published Docker images already default to production).
Required
Section titled “Required”| Variable | What it is |
|---|---|
DATABASE_URL |
The PostgreSQL connection string. |
SPHERICON_JWT_SECRET |
Signs session cookies. Generate it with openssl rand -hex 32. |
SPHERICON_ENCRYPTION_KEY |
Encrypts stored provider credentials. Generate it with sphericon genkey. |
SPHERICON_APP_URL |
The public https:// origin of the Instance. Tracking and unsubscribe links are built from it. |
All variables
Section titled “All variables”| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL |
yes | — | PostgreSQL connection string (or DATABASE_URL_FILE; postgres://user:pass@host:5432/db?sslmode=require). Use sslmode=verify-full (with sslrootcert) to also verify the server certificate. |
SPHERICON_JWT_SECRET |
yes in prod | — | Signing secret for auth tokens (or SPHERICON_JWT_SECRET_FILE). Outside development/test the server refuses to boot if it is empty, shorter than 32 characters, or a known placeholder. Generate one with openssl rand -hex 32. |
SPHERICON_JWT_SECRET_SECONDARY |
no | — | The second secret of a JWT secret rotation (or SPHERICON_JWT_SECRET_SECONDARY_FILE): accepted when verifying sessions and mailed links, never used to sign. Unset outside a rotation. Same rules as SPHERICON_JWT_SECRET, and different from it. See Rotating the JWT secret. |
SPHERICON_SESSION_TTL |
no | 24h |
Lifetime of a dashboard session (a Go duration, e.g. 12h): the session token expires and the cookie is dropped after it, with no refresh. Must be positive. |
SPHERICON_ENCRYPTION_KEY |
yes | — | Base64 Tink keyset (or SPHERICON_ENCRYPTION_KEY_FILE) used to encrypt stored provider credentials. Generate one with sphericon genkey (see Generate the keys). Boot fails if missing. |
SPHERICON_APP_URL |
no | http://localhost:3000 |
Public base URL — used when issuing auth tokens and building tracking/unsubscribe links. Set to your real origin. Its host is also the Passkey relying party id (ADR 0052): changing the host invalidates every registered Passkey, so Users sign in with their password (or reset it) and register their Passkeys again. An Instance served on an IP address cannot use Passkeys, because a relying party id must be a domain name. |
SPHERICON_TRACKER_URL |
no | (empty: SPHERICON_APP_URL) |
Public origin the tracker snippet loads /t.js from and posts /collect to, e.g. https://t.example.com; the Tracking settings page builds its snippet and test command on it. The proxy for that host routes to the same Instance. Leave empty on a single host. A verified Tracking domain of a Workspace serves the same tracker for that Workspace’s collect key (ADR 0059). |
SPHERICON_MCP_URL |
no | (empty: SPHERICON_APP_URL) |
Public origin of a dedicated MCP host, e.g. https://mcp.example.com. The MCP resource is then ${SPHERICON_MCP_URL}/mcp while the OAuth issuer and consent screen stay on SPHERICON_APP_URL. Leave empty on a single host. |
PORT |
no | 3000 |
HTTP listen port. |
SPHERICON_METRICS_ADDR |
no | — (off) | host:port for the opt-in Prometheus listener. Must differ from PORT; see Prometheus metrics. |
SPHERICON_INTERNAL_ADDR |
no | — (off) | host:port for the opt-in internal listener that serves the edge’s on-demand TLS ask (GET /tls/ask?domain=, always, ADR 0038) and the Service credential endpoints of the Staff surface (these need the staff license). Must differ from PORT and SPHERICON_METRICS_ADDR; see Service credentials. |
SPHERICON_TRACKING_CNAME_TARGET |
no | — (off) | The edge host a Sending domain’s Tracking domain CNAME (track.<domain>) must point at (ADR 0038), for example edge.example.com. Unset turns the feature off: no tracking record is shown on a Sending domain. |
SPHERICON_TRACKING_HOST_HEADER |
no | — (off) | Name of the request header your edge sets to the customer’s host (for example X-Forwarded-Host), so a request for a verified Tracking domain is served only link paths and the tracker (/t.js, /collect, for that Workspace’s collect key only; ADR 0038, 0059). Believed only from a peer inside SPHERICON_TRACKING_EDGE_CIDRS; from anyone else it is ignored. Unset: the connection’s Host is used. Have the proxy for the Instance’s own host drop this header. |
SPHERICON_TRACKING_EDGE_CIDRS |
no | loopback only (127.0.0.0/8,::1/128) |
Comma-separated CIDRs the edge connects from: the only peers whose SPHERICON_TRACKING_HOST_HEADER is trusted. A proxy that is not on the same machine must be listed explicitly (for example its Docker network or VPC CIDR); a peer outside the list can not set the host. |
SPHERICON_PRESETS_DIR |
no | — (built-in only) | Directory of custom Presets: every *.yaml / *.yml file at its root loads beside the built-in ones at start, and a file that does not load or reuses a built-in name stops the start. A Workspace never supplies one. Test a file first with sphericon preset test; see Presets. |
SPHERICON_DB_AUTO_MIGRATE |
no | false |
Apply embedded migrations on startup. Convenient for single-replica; see below. |
SPHERICON_DB_MIGRATE_TIMEOUT |
no | 10m |
With auto-migrate on, the longest a start waits for the migration lock and the run before it fails. 0 is no limit. sphericon migrate itself has no limit. |
SPHERICON_CORS_ORIGINS |
no | — | Comma/space-separated origins allowed credentialed CORS on the cookie-authenticated API (/site, /auth). Empty means same-origin only, which is what the bundled SPA needs; the bearer-token APIs are unaffected. |
SPHERICON_MAX_BODY_BYTES |
no | 1048576 |
Largest accepted request body (bytes) on every surface except /collect; larger bodies get 413. |
SPHERICON_COLLECT_MAX_BODY_BYTES |
no | 512000 |
Largest accepted /collect batch (POST /collect/events, bytes); larger gets 413. |
SPHERICON_COLLECT_MAX_EVENT_BYTES |
no | 32768 |
Largest single /collect event (bytes): an identify body, or each event inside a batch; larger gets 413. |
SPHERICON_RATE_LIMIT_HUMAN_PER_MINUTE |
no | 60 |
Requests per minute per IP and endpoint on signup, invitation accept and consent confirm; over it 429 with Retry-After; 0 disables. |
SPHERICON_RATE_LIMIT_API_BURST_PER_SECOND |
no | 20 |
Requests per second per Workspace on /api and /mcp (one shared budget); over it 429 with Retry-After; 0 disables |
SPHERICON_RATE_LIMIT_API_PER_MINUTE |
no | 600 |
Requests per minute per Workspace on /api and /mcp, stacked on the burst limit; 0 disables |
SPHERICON_RATE_LIMIT_FAILED_AUTH_PER_MINUTE |
no | 30 |
Failed bearer-token authentications per minute per IP; over it 429; successful ones are not counted; 0 disables |
SPHERICON_RATE_LIMIT_TRACKING_PER_MINUTE |
no | 600 |
Open and click events recorded per minute per IP; over it the redirect or pixel is still served and only the recording is skipped (never a 429); 0 disables. |
SPHERICON_RATE_LIMIT_LOGIN_FAILURES |
no | 5 |
Failed logins per account within 15 minutes before login answers 429 with Retry-After and a doubling delay (1 s up to 15 min, no lockout), even for a correct password; 0 disables. |
SPHERICON_RATE_LIMIT_LOGIN_IP_PER_MINUTE |
no | 20 |
Login requests per minute per client IP; over it 429 with Retry-After; 0 disables. |
SPHERICON_RATE_LIMIT_COLLECT_PER_MINUTE |
no | 6000 |
Requests per minute per Workspace on /collect (its own budget, apart from /api); over it 429 with Retry-After; 0 disables. |
SPHERICON_RATE_LIMIT_SERVICE_PER_MINUTE |
no | 600 |
Requests per minute per Service credential on the internal listener; over it 429 with Retry-After; 0 disables. |
SPHERICON_RATE_LIMIT_COLLECT_IP_PER_MINUTE |
no | 300 |
Requests per minute per client IP on /collect; over it 429 with Retry-After; 0 disables. |
SPHERICON_RATE_LIMIT_FORGOT_PASSWORD_PER_ADDRESS_PER_HOUR |
no | 3 |
Mails sent per address per hour by forgot-password, by registration and by an email-change request (one budget for each, ADR 0031); over it they still answer 202 and send nothing (the answer never reveals whether the address has an account); 0 disables. |
SPHERICON_RATE_LIMIT_FORGOT_PASSWORD_IP_PER_HOUR |
no | 10 |
Forgot-password requests per client IP per hour; over it 429 with Retry-After; 0 disables. |
SPHERICON_DB_MAX_OPEN_CONNS |
no | 15 |
Maximum open connections in the database/sql pool (API, ent, event bus). |
SPHERICON_DB_MAX_IDLE_CONNS |
no | SPHERICON_DB_MAX_OPEN_CONNS |
Maximum idle connections kept in that pool. Must not exceed SPHERICON_DB_MAX_OPEN_CONNS. |
SPHERICON_DB_CONN_MAX_LIFETIME |
no | 30m |
Maximum lifetime of a pooled connection (Go duration). |
SPHERICON_PGX_MAX_CONNS |
no | 25 |
Maximum connections in the river (job queue) pool. river’s own queries (fetch, completion, LISTEN, leader election) run on it; job workers do their database work through the database/sql pool, so it need not match river’s 20 workers (a small pool only queues completions). |
SPHERICON_OUTBOX_RETENTION_FLOOR_DAYS |
no | 7 |
Minimum age (days) before a domain-event outbox row that every consumer has processed is pruned. The prune job runs every 10 minutes. |
SPHERICON_UNVERIFIED_USER_RETENTION_DAYS |
no | 7 |
Age (days) after which a registration whose email was never verified, and that holds no Membership, is deleted by a daily job at 04:00 UTC, freeing its address; 0 disables. A pending Invitation to that address is kept. |
SPHERICON_EVENTS_RETENTION_DAYS |
no | 400 |
Age (days) after which analytical Events are deleted by a daily job at 03:00 UTC; 0 disables. Evidentiary Events (marketing.confirmed, email.complained, email.unsubscribed, permanent email.bounced) are never deleted by age. Segment conditions that look back past this window are capped by it. |
SPHERICON_EVENT_LIVE_WINDOW_HOURS |
no | 72 |
Age (hours) past which the external API refuses a live Event as history: send it with backfill instead. At least 1; a Backfill is never subject to it. /collect is not limited. |
SPHERICON_TELEMETRY |
no | unset | Decides the anonymous Telemetry report (ADR 0032): true sends it and locks the in-app switch on, false never sends it and locks it off; unset leaves the choice to the Owner (default on). |
DO_NOT_TRACK |
no | unset | 1 switches the Telemetry report off and locks the in-app switch; it can never switch the report on. |
SPHERICON_RELEASE_CHECK |
no | true |
false switches the daily Release check off (ADR 0032): it asks GitHub for the newest stable release and shows Owners an update banner. It sends nothing about the Instance and is independent of the Telemetry report and DO_NOT_TRACK. |
SPHERICON_APP_LOCALE |
no | en |
Instance-wide UI and email language: en is the only one; anything else becomes en. |
SPHERICON_REQUIRE_EMAIL_VERIFICATION |
no | true |
Whether an account must prove its email address before it can sign in (ADR 0030). Only an explicit false turns it off, for an install without outbound mail: registration then takes a password and yields a verified user with its workspace at once. Any other value keeps it on. Never inferred from the SMTP settings. |
SPHERICON_SIGNIN_GOOGLE_CLIENT_ID |
no | none | Enables “Continue with Google” on the login and register pages (ADR 0048): the OAuth client id of your Google Cloud app. Set it together with SPHERICON_SIGNIN_GOOGLE_CLIENT_SECRET; one without the other stops the start. Register <SPHERICON_APP_URL>/site/auth/signin/google/callback as the app’s authorized redirect URI. Google’s email is trusted only for a Gmail address or an account of a hosted domain. |
SPHERICON_SIGNIN_GOOGLE_CLIENT_SECRET |
no | none | The OAuth client secret that goes with SPHERICON_SIGNIN_GOOGLE_CLIENT_ID. |
SPHERICON_SIGNIN_GITHUB_CLIENT_ID |
no | none | Enables “Continue with GitHub” on the login and register pages (ADR 0048): the OAuth client id of your GitHub OAuth app. Set it together with SPHERICON_SIGNIN_GITHUB_CLIENT_SECRET; one without the other stops the start. Set <SPHERICON_APP_URL>/site/auth/signin/github/callback as the app’s authorization callback URL. GitHub’s primary email is trusted only when GitHub marks it verified. |
SPHERICON_SIGNIN_GITHUB_CLIENT_SECRET |
no | none | The OAuth client secret that goes with SPHERICON_SIGNIN_GITHUB_CLIENT_ID. |
SPHERICON_SIGNUP_OPEN |
no | true |
Whether anyone who reaches the instance can register (ADR 0042). Only an explicit false closes it: signup then answers 403 with code signup_closed, and a new user comes only through an invitation or sphericon user create-owner. Any other value keeps it open. |
SPHERICON_SIGNUP_SURVEY |
no | false |
Whether a new user answers a four-question sign-up survey once after the first sign-in (ADR 0055). Only an explicit true turns it on; any other value keeps it off. It needs the staff license feature, because only the Staff console reads the answers: without it the survey stays off and the server logs one warning at start. Users who accept an invitation, the first owner made by sphericon user create-owner and users who existed before the switch are not asked. Answers stay in the instance database and are never part of the telemetry report. |
SPHERICON_LICENSE_KEY |
no | — | Enterprise Edition license key (or SPHERICON_LICENSE_KEY_FILE), a JWT signed with Ed25519 (features claim, standard exp); without it the single binary runs the open-source core only. A key that has expired (valid signature, past exp) does the same instead of failing boot (ADR 0035): the start logs a warning naming the expiry date, the owner’s admin banner shows it and the sphericon_license_expired metric is 1; no Enterprise data is deleted, and setting a valid key and restarting restores the features with it. A malformed key or one whose signature does not verify still fails boot. |
SPHERICON_LOG_LEVEL |
no | info |
Log level. |
SPHERICON_LOG_FORMAT |
no | json (text in development) |
Log format: json or text. |
OTEL_SERVICE_NAME |
no | sphericon |
Service name reported to OpenTelemetry. |
OTEL_CONFIG_FILE |
no | unset (no OTLP export) | Path to an OpenTelemetry declarative configuration YAML file: you choose which traces, metrics and logs go to which destination. |
SPHERICON_SMTP_HOST / SPHERICON_SMTP_PORT / SPHERICON_SMTP_USER / SPHERICON_SMTP_PASS / SPHERICON_SMTP_FROM |
no | SPHERICON_SMTP_PORT=1025 |
Outbound email over SMTP (SPHERICON_SMTP_PASS_FILE for the password). |
SPHERICON_SMTP_ALLOW_PRIVATE_HOSTS |
no | false in production, true otherwise |
Let a Workspace’s SMTP integration use a loopback or private host and any port. Off in production: tenants may only reach public hosts on ports 25, 465, 587 and 2525. The platform’s own SPHERICON_SMTP_HOST is exempt. |
SPHERICON_OUTBOUND_ALLOW |
no | — (nothing allowed) | Comma-separated host:port pairs (a name or an IP address, a port each; no wildcards or ranges) that outbound calls (Webhook endpoint deliveries, test events, Automation HTTP steps and the fetch of an MCP client’s metadata document, which any visitor can trigger at /oauth/authorize) may reach although the address is private or loopback, e.g. n8n:5678,node-red:1880, so events can go to a service on your own network. Off by default; redirects are still never followed. A malformed value stops the server at boot. Leave it unset on a multi-tenant host. |
SPHERICON_SYSTEM_EMAIL_PROVIDER |
no | smtp |
Platform (system) email provider: smtp or ses. |
SPHERICON_SYSTEM_EMAIL_FROM |
no | noreply@sphericon.localhost |
From address for platform mail (e.g. welcome emails). |
SPHERICON_SES_REGION / SPHERICON_SES_ACCESS_KEY_ID / SPHERICON_SES_SECRET_ACCESS_KEY |
no | — | Credentials of an SES-compatible service when using the ses provider. |
SPHERICON_COLLECT_SITE_KEY |
no | — | Tracker ingestion key. |
SPHERICON_BOOTSTRAP_TOKEN |
no | — | External-API bootstrap token. |
SPHERICON_STAFF_JWT_SECRET |
yes with the staff license |
— | Signs the platform Staff member session and login challenge (ADR 0026). Required only when the license has the staff feature, then at least 32 characters and different from SPHERICON_JWT_SECRET, or the server refuses to boot. Generate one with openssl rand -hex 32. |
SPHERICON_ACCOUNT_LINK_URL |
no | — (off) | Address of an external account page. With SPHERICON_ACCOUNT_LINK_SIGNING_KEY, a Workspace Owner sees an Account link that opens it with a signed token; set both or neither. The app never calls this address. |
SPHERICON_ACCOUNT_LINK_SIGNING_KEY |
no | — (off) | Base64 Ed25519 seed that signs the Account link token (generate with openssl rand -base64 32). The app publishes the matching public key at /.well-known/jwks.json, so the receiver needs no secret. |
SPHERICON_STAFF_SESSION_TTL |
no | 4h |
Lifetime of a Staff member session (a Go duration): shorter than SPHERICON_SESSION_TTL, with no refresh and no “remember me”. Must be positive. |
SPHERICON_STAFF_HOLD_MAX_EXPIRY |
no | 720h |
How far ahead a service may set an Staff hold’s expiry (a Go duration); a later expiry is refused. A hold that is not renewed lapses by itself. Must be positive. |
SPHERICON_ABUSE_VOLUME_FLOOR |
no | 1000 |
Abuse detector (staff license, ADR 0026): the fewest sends in the trailing 24 hours, per Sending domain, for its rates to be judged; below it nothing is warned or frozen. Must be positive. |
SPHERICON_ABUSE_WARN_COMPLAINT_PERCENT |
no | 0.1 |
Complaint rate (percent) at which the abuse detector warns the Workspace owners by email, freezing nothing. Above 0 and below the freeze limit. |
SPHERICON_ABUSE_WARN_BOUNCE_PERCENT |
no | 5 |
Bounce rate (percent) at which the abuse detector warns the owners. Above 0 and below the freeze limit. |
SPHERICON_ABUSE_FREEZE_COMPLAINT_PERCENT |
no | 0.3 |
Complaint rate (percent) at which the abuse detector suspends the whole Workspace (actor system) and tells its owners. At most 100. |
SPHERICON_ABUSE_FREEZE_BOUNCE_PERCENT |
no | 10 |
Bounce rate (percent) at which the abuse detector suspends the whole Workspace. At most 100. |
SPHERICON_SUPPORT_EMAIL |
no | — (none) | Support contact shown to a Workspace owner with a suspension (banner and notice email) so they can appeal by email. Empty shows none. |
Secrets from files
Section titled “Secrets from files”Docker and Kubernetes mount secrets as files. DATABASE_URL, SPHERICON_JWT_SECRET, SPHERICON_JWT_SECRET_SECONDARY,
SPHERICON_ENCRYPTION_KEY, SPHERICON_SMTP_PASS and SPHERICON_LICENSE_KEY each accept a _FILE
sibling (SPHERICON_JWT_SECRET_FILE=/run/secrets/jwt) whose value is a path read at start. The
trailing newline is trimmed. A value set directly wins over the file, which is then never read; an
unreadable or empty file fails boot with an error naming the _FILE variable.