Environment variables
About 2029 wordsAbout 7 min
Spora Environment Variables
This is the canonical reference for every SPORA_* environment variable Spora reads at boot. Both the Deployment modes and Worker deployment pages cross-link to this doc for env-var details.
Resolution priority: built-in defaults → config.php (gitignored) → .env / OS environment → the settings table. SPORA_* env vars beat config.php but not the database: for every setting that is not read before the database opens, a value an admin saved at Settings → Runtime settings overrides the environment.
The exception is the bootOnly family — SPORA_DB_*, SPORA_KEY_PATH, SPORA_LOG_PATH, SPORA_APP_ENV, SPORA_APP_URL, SPORA_APP_PREFIX, SPORA_SQLITE_BUSY_TIMEOUT — which is read before the database can be opened and is therefore file-only. For those an env var genuinely is the highest priority. See Config keys for the full chain.
Quick links: SERVER_NAME · Application · Encryption · Database · Worker runtime mode · Timeouts · Mercure (SSE) · Logging · Notifications / Mail · Plugins · Config path
SERVER_NAME
| Variable | Default | Config key | Description |
|---|---|---|---|
SERVER_NAME | — | — | Read by FrankenPHP / Caddy. Not a SPORA_* env var — set it directly in .env / Compose. In development set localhost:80; in production set the public domain so Caddy auto-issues Let’s Encrypt and redirects HTTP → HTTPS. |
Application
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_APP_URL | auto-detected | app_url | Full public URL of this instance, including non-standard port. Used for verification / password-reset email links. Resolution order — first wins: config.php app_url → SPORA_APP_URL env var → the request’s HTTP_HOST → the web server’s SERVER_NAME (Apache ServerName) → http://localhost. Never reads X-Forwarded-* headers — those are spoofable and Spora has no trusted-proxy allowlist. Operators behind a reverse proxy that rewrites Host MUST set this. For shared hosting where the env var is awkward, set it in config.php instead. |
SPORA_APP_PREFIX | /spora | app_prefix | Path prefix Spora is mounted under. Default is /spora because the admin UI ships under public/spora/ and plugins ship under public/plugins/<name>/. Operators running Spora at the host root (e.g. when developing their own frontend) MUST set SPORA_APP_PREFIX="" to opt out. Leading/trailing slashes are stripped; bare / collapses to empty. Prepended to email-link paths (verification, password reset). For shared hosting, set app_prefix => '/spora' in config.php instead. |
SPORA_APP_ENV | production | app_env | production, development, or testing. Read at app/Core/ContainerDefinitions.php:394 and defaulted to production at app/Core/ContainerDefinitions.php:248 (and config.php:31). Consumers: Kernel::configureErrorHandling() and the debug payload in Kernel::handleException() (development or local adds a debug block to 500 responses), plus the login-throttling toggle at app/Core/ContainerDefinitions.php:506, which is enabled for every value except testing. |
SPORA_ALLOW_REGISTRATION | true | allow_registration | Whether POST /api/v1/auth/register is open. Set to false once you have created your account. |
Encryption
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_SECRET_KEY | — | — | Base64-encoded 32-byte master key for libsodium secretbox encryption of tool credentials. Required for production. Generate: php -r "echo base64_encode(random_bytes(32));". Never commit. Losing it means losing access to encrypted settings. |
SPORA_KEY_PATH | storage/secret.key (auto-generated by php bin/spora spora:install / php bin/spora db:seed) | key_path | Alternative: path to a 32-byte binary key file. Overrides the default install path. On a fresh checkout, the key is auto-generated the first time you run php bin/spora spora:install or php bin/spora db:seed. |
See Security for the full key-resolution chain and algorithm.
Database
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_DB_DRIVER | sqlite | db_driver | sqlite (zero-config), mysql, or mariadb. |
SPORA_DB_HOST | 127.0.0.1 | db_host | MySQL/MariaDB host. |
SPORA_DB_PORT | 3306 | db_port | MySQL/MariaDB port. |
SPORA_DB_NAME | spora | db_name | MySQL/MariaDB database name. |
SPORA_DB_USER | spora | db_user | MySQL/MariaDB user. |
SPORA_DB_PASSWORD | — | db_password | MySQL/MariaDB password. |
SPORA_SQLITE_BUSY_TIMEOUT | 5000 | sqlite_busy_timeout | SQLite only. Wait time in ms when the database is locked. |
SQLite path is set in config.php (defaults to storage/database.sqlite).
The DB driver is selected by SPORA_DB_DRIVER (sqlite, mysql, or mariadb) and is also what php bin/spora db:reset reads to decide whether to wipe the local SQLite file or run DROP DATABASE + CREATE DATABASE on the configured MySQL/MariaDB server. See Install for the destructive db:reset flow.
Worker runtime mode
Spora has two runtime modes — server (a daemon drains the queue) and client (the browser’s SharedWorker drives the user’s own tasks). For the full three-configuration overview (server + Mercure, server + polling, client + polling), see Deployment modes.
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_WORKER_RUNTIME_MODE | server | worker_runtime_mode | server = bin/spora worker:run drains the queue (daemon or cron). client = the browser’s SharedWorker calls /api/v1/tasks/{id}/tick for tasks the user ran. Default is server; set to client for shared hosts without a daemon. A future curated spora-shared skeleton will ship with client as its baked-in default. |
SPORA_TICK_LEASE_SECONDS | 600 | tick_lease_seconds | Client-mode only. Lease TTL (seconds) for in-flight ticks. Written to lease_owner + lease_expires_at by TaskTickController::tick and ScheduledRunProcessor::dispatchAndTick. Extended at every orchestrator step boundary so a healthy tick never lapses. Server-mode daemon paths skip the lease entirely (rely on updated_at), so this var is read but not consulted in server mode. The reaper flips a client-mode row to FAILED with error_code: WORKER_DISCONNECTED once the lease expires + SPORA_WORKER_STALE_MINUTES elapses with no progress. |
SPORA_WORKER_STALE_MINUTES | 60 | worker_stale_minutes | Grace period (minutes) before an orphan RUNNING task is reaped. In client mode this applies after the lease has expired; in server mode it applies directly to updated_at (no lease was granted). Should exceed worst-case LLM round-trip time. Set to 0 to disable the reaper. |
SPORA_MAX_WORKERS | 0 | max_workers | Max concurrent child processes in daemon mode (0 = unlimited). No effect in client mode — the browser handles concurrency per tab. |
Default mode.
spora-ai/sporashipsworker_runtime_mode: serveras the safe default for Docker/VPS/classical-server operators who run the daemon. For shared-host operators without shell access to run a long-lived PHP process, setSPORA_WORKER_RUNTIME_MODE=clientin.env. There is one Composer package today; a curatedspora-sharedskeleton (client-default out of the box, nodocker//supervisord.conf/Mercure) is on the roadmap. See Installation modes for the one-screen picker.Removed in 0.19.0:
SPORA_SYNC_MODE. Pre-0.19 used a booleanSPORA_SYNC_MODE(true= inline / dev,false= queued / worker). The boolean is gone. To get the inline-tick behaviour pre-0.19 operators got fromSPORA_SYNC_MODE=true, run withSPORA_WORKER_RUNTIME_MODE=serverand either invokephp bin/spora worker:run --daemon(a request’s task is picked up on the next polling cycle) or use the dev-modebin/sporaCLI’s direct task runner. The HTTP request always returns once the task isQUEUED; the worker drives the result. For shared hosts, setSPORA_WORKER_RUNTIME_MODE=clientin.envinstead.
See Worker deployment for server-mode cron / daemon / supervisord / systemd patterns and the --stale-minutes / --workers CLI flag overrides. See Client-worker mode for the client-mode shared-host guide, lease semantics, and browser support.
Timeouts
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_LLM_TIMEOUT | 300 | llm_timeout | Seconds for LLM API calls. Reasoning models may need 300+. |
SPORA_TOOL_HTTP_TIMEOUT | 30 | tool_http_timeout | Seconds for tool HTTP requests (web search, calendars, etc.). Per-tool overrides via the http_timeout setting on each tool’s #[ToolSetting] in agent tool settings. |
Mercure (SSE)
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_MERCURE_URL | — | mercure_url | Public Mercure hub URL the browser subscribes to. Omit to disable SSE. |
SPORA_MERCURE_PUBLISH_URL | falls back to SPORA_MERCURE_URL | mercure_publish_url | Publisher endpoint. In Docker with a public SERVER_NAME, use http://localhost:80/.well-known/mercure (loopback) so the in-container POST matches the Caddy site address. Avoid http://spora:80/... (bakes the docker-compose service name into the image) and http://localhost/... (no port → Caddy auto-HTTPS redirects to a port the in-container client can’t reach). |
SPORA_MERCURE_JWT_KEY | — | mercure_jwt_key | HS256 shared secret for Mercure publisher and subscriber tokens. |
SPORA_MERCURE_LEGACY_USER_TOPIC | false | mercure_legacy_user_topic | Plan B fan-out rollout flag. When true, the publisher also emits task events to the legacy user/{userId}/tasks topics for every visible user, in addition to the principal-keyed principal/{principalId}/tasks topics. Set to true if you deploy the spora-core backend before the spora-frontend cutover (frontend versions older than v0.15.0 still subscribe to the legacy topics). Will be removed in the release after the frontend cutover. |
When all three primary vars are unset, MercurePublisher::publish() early-returns and the frontend falls back to polling. See Agent loop and async mode for the SSE topic model.
Logging
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_LOG_LEVEL | warning | log_level | debug | info | warning | error. Tool arguments (potentially PII) are logged at DEBUG — use info or higher in production. |
SPORA_LOG_PATH | storage/spora.log | log_path | Docker deployments should set stdout so records stream to the container’s log driver. The framework default storage/spora.log is the env-less LAMP/FTP fallback. |
Note: storage/php.log is not produced by Spora’s logger. In local dev it is the redirected stdout/stderr of the PHP built-in server started by bin/dev. In production Spora does not configure the PHP error_log directive; set it via php.ini / supervisord / your container entrypoint if needed. See Logging.
Notifications / Mail
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_NOTIFICATIONS_EMAIL_ENABLED | true | notifications_email_enabled | Send an email when a scheduled run completes. Requires the SPORA_MAIL_* block to be configured. |
SPORA_PLUGIN_INSTALL_ENABLED | false | plugin_install_enabled | Enable the Web UI for plugin install/uninstall (POST/DELETE/PATCH /api/v1/plugins). When off, the routes return 403 FEATURE_DISABLED. CLI commands (php bin/spora plugin:install, plugin:uninstall, plugin:update) are not gated — they’re always available as the operator recovery path. See Plugin install API. |
Mail transport has three ways to be set: these env vars (read by app/Services/SystemMailer.php:183-190 and app/Http/MailConfigController.php:22-29), the same keys in config.php (nested under mail, or flat as SPORA_MAIL_*), and Settings → Runtime settings → Mail & Realtime, which stores them encrypted in the database and wins over both. SPORA_MAIL_PASSWORD is write-only — the server never returns its value, not even to an admin. These are not in the default .env.example; they are only in docker/.env.local.example. The full set is SPORA_MAIL_DRIVER / SPORA_MAIL_HOST / SPORA_MAIL_PORT / SPORA_MAIL_USERNAME / SPORA_MAIL_PASSWORD / SPORA_MAIL_ENCRYPTION / SPORA_MAIL_FROM / SPORA_MAIL_FROM_NAME.
For SMTP, use a bare hostname in SPORA_MAIL_HOST. Pair port 587 with SPORA_MAIL_ENCRYPTION=tls for STARTTLS, or port 465 with SPORA_MAIL_ENCRYPTION=ssl for implicit TLS/SMTPS. Using ssl on port 587 causes OpenSSL wrong version number errors because that port expects an SMTP greeting before the TLS upgrade.
Agent API
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_AGENT_API | false | — | Mounts the per-agent /api/v1/agent-api/* surface for external tools. Requires a server-side worker — in client-worker mode every endpoint answers 404, whatever this is set to. Keys are managed in the agent’s Settings → API keys section. See Agent API. |
SPORA_AGENT_API_RATE_LIMIT_MAX | 60 | — | Requests per minute a single Agent API key may make. Exceeding it answers 429 with a Retry-After header. |
Neither has a config.php key of its own; the only spelling config.php accepts is the flat 'SPORA_AGENT_API' => true. Both are plain (non-bootOnly) settings, so once either has been saved in Global Settings the stored value wins over .env — set it in one place, not both.
Plugins
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_COMPOSER_BINARY | composer | composer_binary | Path to the composer executable that the install/uninstall commands shell out to. Absolute paths ending in .phar are prefixed with php automatically. |
SPORA_PLUGIN_CATALOG_ENABLED | true | plugin_catalog_enabled | When false, the Browse tab in /apps/plugins is hidden and GET /api/v1/plugins/catalog returns 404. Already-installed plugins are unaffected — only the discovery surface is gated. |
SPORA_PLUGIN_CATALOG_TTL | 3600 | plugin_catalog_ttl | Cache TTL (seconds) for the on-disk Packagist cache at storage/.spora_plugin_catalog.json. Different queries share the file but each gets its own entry keyed by a SHA-256 fingerprint of the query string. |
See Plugin author guide → Distribution for what authors need to ship so their plugin shows up under Browse, and Plugin system for the runtime side (manifest, auto-discovery).
Config path
| Variable | Default | Config key | Description |
|---|---|---|---|
SPORA_CONFIG_PATH | (default install path) | config_path | Override the location of the config.php file. The installer uses this when no config.php exists yet. |