PORT | number | 3000 | Port the single HTTP listener binds. |
HOST | string | 0.0.0.0 | Bind address. |
PUBLIC_BASE_URL | string | http://localhost:3000 | Public origin of this instance (scheme matters: https => Secure cookies). |
VIEWER_BASE_URL | string | — | Base URL share links point at (the /v/{secret} viewer). Unset (default) = same origin as PUBLIC_BASE_URL — the proven-safe default: user HTML only ever renders under Content-Security-Policy: sandbox (opaque origin, never allow-same-origin). Setting this to a dedicated user-content origin (a domain that carries no app cookies and no API, fronting the same instance) is the documented hardening path (docs/security/viewer-security-model.md): share URLs are then built on that origin, that hostname serves ONLY decks and the token-authed viewer API (the dashboard, login, /mcp and the rest of /api/v1 answer 404 there; deck links on the app hostname redirect across), the app API refuses requests carrying the viewer origin, and a header regression can no longer expose the dashboard session across a real origin boundary. Must differ from PUBLIC_BASE_URL’s origin. |
VIEW_DEDUPE_WINDOW_MINUTES | number | 10 | De-dupe window (minutes) for share-link view counting: repeat opens of the same link from one browser inside this window count once, so browser prefetch/prerender, reloads, and mail-scanner hits no longer inflate a token’s accessCount. Enforced with a signed, token-scoped HttpOnly cookie; cookie-less clients (SDKs, curl) count every fetch. Large values shift the metric toward “unique browsers” rather than “opens”. 0 disables de-dupe: every entry GET counts and no cookie is set. |
DATA_DIR | string | /data | Writable data directory (auto-generated secret, local file storage). |
AUTO_MIGRATE | string | true | Apply pending migrations at boot. When false the app only checks and refuses readiness while behind. |
SERVICE_ROLE | all | api | worker | all | all = API + workers in one process; api = HTTP only; worker = jobs only. |
AUTH_SECRET | string | — | Session/JWKS encryption secret. Auto-generated into DATA_DIR/secret when unset or empty. |
API_KEY_PEPPERS | string | — | Versioned API-key peppers for secret rotation: <version>:<secret> entries joined by ; (e.g. 1:<historical AUTH_SECRET>;2:<new pepper>, secrets >=32 chars). Version 1 defaults to AUTH_SECRET and, when pinned here, MUST keep the historical AUTH_SECRET-derived value or every existing key stops resolving; new keys mint under the highest version. Rotation runbook: internal/security-runbooks.md. |
SETUP_TOKEN | string | — | The credential POST /api/v1/setup requires (constant-time compared). setup.sh generates one into .env. Unset, the server generates a token into $DATA_DIR/setup-token at first boot and prints it to the container log — the first-boot claim is never free (PRDCT-1347). |
ALLOW_INSECURE_SETUP | string | false | Allow POST /api/v1/setup over plaintext HTTP on a non-loopback PUBLIC_BASE_URL. Default false: the wizard 403s, because the owner password and the setup token would cross the network in the clear on the one request that decides who owns the instance. Reach a TLS-less server through an SSH tunnel (ssh -L) instead; set this true only on a trusted private network. |
SUPERADMIN_EMAILS | string | — | Break-glass operator allowlist: comma-separated emails. A caller is superadmin ONLY on a SESSION whose VERIFIED email is listed here — machine credentials (API keys, OAuth tokens) never qualify, they 403 fail-closed. Unset (default) = the break-glass endpoints are dormant and 403 for everyone. Runbook: internal/security-runbooks.md. |
GOOGLE_CLIENT_ID | string | — | Optional Google social login. |
GOOGLE_CLIENT_SECRET | string | — | |
EDITION | oss | cloud | oss | Edition selector (internal/federation.md): oss (default, self-host — zero hub surface at runtime) or cloud (federates human login + entitlements to the Antasphere hub; requires the HUB_* block). Any other value refuses to boot — the selector decides the identity binding, so a typo must fail loudly, never silently bind oss. Also surfaced in discovery + usage events. |
HUB_ISSUER_URL | string | — | Hub OIDC issuer, e.g. https://account.antasphere.com — discovery, JWKS, and the authorize/token endpoints all derive from it. Required when EDITION=cloud; never read when EDITION=oss. |
HUB_CLIENT_ID | string | — | OAuth client id from this tool’s entry in the hub TOOL_REGISTRY (e.g. tool-slideless-cloud). Required when EDITION=cloud. |
HUB_CLIENT_SECRET | string | — | OAuth client secret matching the hub registry entry (confidential client; PKCE stays on regardless). Also authenticates the per-user refresh grant — there is NO service key: every hub read between logins presents the USER’s own grant (internal/federation.md). Required when EDITION=cloud. |
HUB_HINT_COOKIE_NAME | string | — | Name of the hub-set shared SSO hint cookie the dashboard reads client-side (internal/federation.md; NEVER a security input — it only gates whether a silent connect is attempted). Cloud-only; unset = the cross-repo default ant_sso_hint. Never read when EDITION=oss. |
HUB_HINT_COOKIE_DOMAIN | string | — | Domain the hint cookie lives on (the hub sets it, tools clear it — both sides must agree). Cloud-only; unset = the hub issuer host minus its first label (account.antasphere.com → antasphere.com). Never read when EDITION=oss. |
EDITION_CHANGE_ALLOWED | string | false | R7 escape hatch (internal/federation.md): acknowledge an EDITION change on an already-set-up instance. Without it, boot refuses an EDITION that differs from the one stamped at setup — flipping editions under existing users/workspaces changes identity semantics and must be a conscious operator act. |
APP_VERSION | string | 0.3.0 | Reported version. Defaults to the code’s own baked-in package version (PRDCT-1844) — the image knows its version intrinsically, CI injects nothing. The env var remains as a deliberate operator override only. |
OAUTH_DYNAMIC_CLIENT_REGISTRATION | string | true | RFC 7591 dynamic client registration on the built-in authorization server: unauthenticated POST /api/v1/auth/oauth2/register, which is how MCP clients self-register. Default true (the connector-friendly posture, rate-limited in api/index.ts). Set false on an instance whose OAuth clients are provisioned by hand — the endpoint then refuses every caller instead of minting client records for anyone who asks. |
HSTS_MAX_AGE | number | 15552000 | Strict-Transport-Security max-age in seconds, sent on every response when PUBLIC_BASE_URL is https (browsers ignore HSTS over plain http per RFC 6797 §7.2, so an http instance is unaffected). Default 180 days; 0 disables the header — the escape hatch for an operator who is not yet certain every subdomain can serve TLS. |
MAX_FILE_SIZE_MB | number | 100 | Instance-level cap read by the default AllowAllEntitlements. |
API_RATE_LIMIT_PER_MINUTE | number | 600 | General per-principal API quota: sustained requests/minute allowed to every authenticated /api/v1 principal (API key, OAuth token, session). 0 disables the general limiter. |
API_RATE_LIMIT_BURST | number | 100 | Spike cap for the general API quota: max requests per principal in any 1-second burst. 0 disables burst smoothing (the per-minute window still applies). |
EMAIL_DRIVER | none | smtp | resend | none | Email delivery. none (default) never blocks a flow: links stay copyable. |
SMTP_URL | string | — | smtp(s)://user:pass@host:port — required when EMAIL_DRIVER=smtp. |
RESEND_API_KEY | string | — | Required when EMAIL_DRIVER=resend. |
EMAIL_FROM | string | — | Sender, e.g. Slideless <noreply@slideless.app>. Required when a driver delivers. |
REDIS_URL | string | — | When set, rate limits (and later caches) are shared across replicas. |
STORAGE_DRIVER | local | s3 | local | File storage: local (default; DATA_DIR volume, single replica) or s3 (MinIO/AWS/R2). |
S3_BUCKET | string | — | |
S3_REGION | string | — | |
S3_ENDPOINT | string | — | Endpoint override for MinIO/R2; leave unset for AWS. |
S3_ACCESS_KEY_ID | string | — | |
S3_SECRET_ACCESS_KEY | string | — | |
S3_FORCE_PATH_STYLE | string | true | Path-style addressing — required by MinIO and most S3-compatibles. |
TRUST_PROXY | string | false | Trust x-forwarded-for for client IPs (rate limits, audit). The app reads the RIGHTMOST hop — the one your proxy appended or set (Caddy ≥2.5 discards client-supplied X-Forwarded-* by default). Enable ONLY behind a reverse proxy you control; when false the socket address is used and spoofed headers are ignored. |
OTEL_EXPORTER_OTLP_ENDPOINT | string | — | OTLP/HTTP endpoint for trace export. Unset (default) = no export, zero phone-home. |
METRICS_TOKEN | string | — | GET /metrics requires Authorization: Bearer <token>; unset = /metrics disabled (401). |
AUDIT_RETENTION_DAYS | number | 365 | Days of audit_log to keep (nightly purge at 03:00). 0 = keep forever. |
VIEW_EVENTS_RETENTION_DAYS | number | 90 | Days of per-view share-link analytics events (share_token_views: when a link was opened, referring site host, placement label, browser family — never IPs or full URLs) to keep. Nightly purge at 03:00; 0 = keep forever. |
ORPHAN_USER_RETENTION_HOURS | number | 72 | Grace period for orphaned users (accounts with ZERO workspace memberships, e.g. setup-race losers): the nightly 03:00 sweep deletes them once older than this many hours. A user with ANY membership row — even deactivated — is never touched. 0 = sweep disabled. |
LOG_LEVEL | fatal | error | warn | info | debug | trace | info | pino level. |
NODE_ENV | development | test | production | production | |