Manual contentsReferenceBrowse 113 chapters
Manual 28 min read

Environment Variables

This is the audited list of every environment variable the Suprnova framework reads at runtime, grouped by the subsystem that consults it. Every entry has been validated against framework source - defaults, types, and behaviour reflect what the code actually does, not what the starter .env happens to ship.

The list also covers the variables the suprnova CLI binary reads (dev server, SSR worker) since those appear in the starter .env and readers will look for them here.

See Configuration for the loading rules (.env → .env.<environment> → process env), the env* helpers (env, env_required, env_optional), and the typed Config::* registration pattern.

Conventions

  • Default - the value the framework uses when the variable is unset. none means there is no default; the framework either errors at boot, falls back to a feature default (e.g. Memory driver), or treats the value as None.
  • Type - the Rust type the variable is parsed into. bool values accept true/false/1/0/yes/no/on/off (case-insensitive). Out-of-range or unparseable values for typed framework knobs are clamped (workflow), warn!-logged then defaulted (lenient env() / env_optional()), or fail boot (strict try_from_env).
  • Required - boot means the framework refuses to start without it in the listed environments. driver means it's required only when the parent driver is selected (e.g. MAIL_SES_REGION is irrelevant unless MAIL_DRIVER=ses). Everything else is optional.

Where a starter .env ships a key the framework never reads (MAIL_FROM_ADDRESS, FILESYSTEM_DISK), it's called out at the end of this chapter.

Application

The APP_* family is the framework's identity and crypto root. These are the variables every Suprnova app sets; the rest of the file becomes relevant as you opt into subsystems.

Var Default Type Purpose
APP_NAME "Suprnova Application" String Application name. Used as the TOTP issuer (2FA), the HTTP Basic WWW-Authenticate realm, mail subject branding, and structured-log fields.
APP_ENV local String Drives Environment::detect() and .env.<suffix> lookup. Recognised aliases (case-insensitive): local, development/dev, staging/stage/stg, production/prod, testing/test. Any other value is preserved as Environment::Custom(...) with original casing.
APP_DEBUG env-aware (see Required) bool Verbose error pages + extra logs. Default is true in local/development/testing and false everywhere else (including staging, production, and any unrecognised custom environment). An explicit value always wins; an unparseable value falls back to the env-aware default with a warn!. The strict try_from_env variant aborts boot on a parse failure.
APP_URL "http://localhost:8765" (AppConfig) / "http://localhost" (URL fallback) String Base URL for absolute URL generation, signed URLs, and Inertia redirects. Trailing slashes are trimmed on read.
APP_KEY none - required in non-dev String (base64-url-no-pad, 32 bytes) AES-256-GCM key for Crypt, encrypted sessions, pagination cursors, signed URLs, and any other encrypt-at-rest path. Boot fails closed when missing or malformed outside local/development/testing. Generate with suprnova key:generate.
APP_KEY_PREVIOUS none String (comma-separated base64 keys, max 8) Comma-separated previous keys used during rotation. Crypt::decrypt tries the current APP_KEY first, then each entry in order. Hard cap of 8 entries - crypto::MAX_PREVIOUS_KEYS. A half-rotated entry that fails to decode aborts boot. See Encryption.
APP_PREVIOUS_KEYS none String (alias of APP_KEY_PREVIOUS) Laravel-compat alias accepted so a Laravel .env dropped into a Suprnova deploy still graceful-decrypts legacy data. When both are set with different values, APP_KEY_PREVIOUS wins with a warn! to surface the duplicate; identical values are accepted silently.
APP_BASE_PATH current working directory Path Root directory the path resolver uses for config/, database/, public/, storage/, resources/, lang/. Useful when running the binary from a different CWD than the project root (e.g. systemd unit, WorkingDirectory= not pointing at the project). Falls back to CWD, then . if CWD is unavailable.
APP_TRUSTED_PROXIES none - empty allowlist String (comma-separated IPs and CIDR ranges) Addresses and ranges of the proxies whose X-Forwarded-* / X-Real-IP headers Request::ip() and the host / scheme / port accessors may believe. An entry is an address (10.0.0.5) or a range in CIDR form (173.245.48.0/20, 2400:cb00::/32). A range of every address (0.0.0.0/0, ::/0) is refused. Empty by default, so proxy headers are ignored and the TCP peer always wins - see the note below before deploying behind a proxy. An entry that is neither an address nor a valid range fails boot (try_from_env).
AUTH_GUARD "web" String Name of the default guard read by Auth::*. Mirrors Laravel - only the default is env-selectable; named guards live in code via AuthConfig::guard(name, …).

Two more APP_* variables - APP_LOCALE and APP_FALLBACK_LOCALE - are read by the localization subsystem rather than by AppConfig, so they are listed under Localization below.

Behind a reverse proxy, set APP_TRUSTED_PROXIES

Ignoring proxy headers is the safe default - X-Forwarded-For is caller-supplied and trusting it unconditionally lets anyone claim any address. But the moment a terminating proxy is in front of you (nginx, Traefik, an ALB, Cloudflare), the TCP peer is the proxy, on every request, and leaving this unset does not merely lose the client's address:

  • Per-IP rate limits collapse into one bucket. ThrottleRequestsMiddleware::with(...) keys on request.ip() and the request path, so ThrottleRequestsMiddleware::with(20, 1, "login") stops meaning "20 login attempts per client per minute" and starts meaning 20 in total, across everyone. ThrottleRequestsMiddleware::default() keys on the user id when a user is signed in and on request.ip() otherwise, so it collapses the same way for every visitor who is not signed in. That is both weaker (no per-attacker budget) and actively dangerous: any single caller can spend the quota and lock every legitimate user out of the login form. See Rate limiting.
  • Request::host(), scheme() and port() fall back to the connection rather than to X-Forwarded-Host / -Proto / -Port, so generated absolute URLs can name the internal address and scheme instead of the public one.

List the addresses the proxy hops reach you from - not the client's. An entry is one address or a range in CIDR form:

APP_TRUSTED_PROXIES=10.0.0.5,173.245.48.0/20,2400:cb00::/32

Four rules decide how the list is used:

  • List every proxy of the chain, not the last one alone. Request::ip() reads X-Forwarded-For from the right and returns the first address that is not in the list. A proxy that is not listed is taken for the client, and all of its clients share one address. This includes an address that a load balancer adds behind the client's, as the external Application Load Balancer of Google Cloud does.
  • A range must hold proxies and nothing else. A client that connects from a trusted address is believed like a proxy: it writes its own X-Forwarded-For, and with it the address ip() returns, and the forwarded host and scheme as well. The network of the pods of a cluster and the range of a VPN have clients in them. Do not list them.
  • The proxy must write X-Forwarded-For itself, by adding the address it saw to the header or by replacing the header. A proxy that passes the client's header on as it came lets the client write all of it.
  • X-Real-IP is read only when the request has no X-Forwarded-For at all. A proxy that writes X-Real-IP alone must remove the client's X-Forwarded-For. The Forwarded header of RFC 7239 is not read.

An IPv4 address written as an IPv6 one (::ffff:10.0.0.5) counts as the IPv4 address, in the headers and for the peer. Request::ips() returns the whole chain. It is a record, not something to decide by.

Nothing detects a missing list for you: an app behind a proxy with the variable unset looks healthy, serves correctly, and quietly rate-limits everyone as one user.

App-key required matrix

Environment APP_KEY required at boot
local no (generates an ephemeral key if missing)
development no
testing no
staging yes - boot exits non-zero with a remediation message
production yes
Custom(...) yes - anything not in the safe-list is treated as production for this check

Server

The HTTP listener and request body limits.

Var Default Type Purpose
SERVER_HOST "127.0.0.1" String Bind address. Set to 0.0.0.0 to expose outside the loopback interface (e.g. in containers).
SERVER_PORT 8765 u16 Bind port. Lenient parse warns and defaults; strict try_from_env aborts boot on a typo.
SERVER_MAX_BODY_SIZE 8388608 (8 MiB) usize (bytes) Process-global maximum request body size. Per-FormRequest::max_body_bytes overrides still apply on individual endpoints. The configured value is wired into the global cap during Server::from_config.
SERVER_MAX_CONNECTIONS unset (unbounded) usize Cap on concurrently active TCP connections. Unset means no cap. A value that is zero or unparseable falls back to a finite 10000 with a warning rather than silently reverting to unbounded - a botched limit is still a request for a limit.
SERVER_HEADER_READ_TIMEOUT 30 u64 (seconds) Deadline for reading a request's complete head. The slowloris mitigation. Zero is treated as invalid, not as "disable", and falls back to the default. Does not apply to established WebSocket/SSE connections.
SERVER_HEALTH_READINESS_TOKEN unset (readiness is public) String Shared secret required to reach /_suprnova/health/ready and /_suprnova/health?db=true, sent as X-Suprnova-Health-Token. Without it those paths answer 404, indistinguishably from any unrouted path; liveness stays public. See Deployment.

Database

Connection URL and sqlx pool tuning. DATABASE_URL is required for any subcommand that touches the database (migrate*, db:sync, db:seed, queue:work with QUEUE_DRIVER=database, workflow:work, the session DB store) and for serve when the app has migrations registered. The five liveness knobs are how you survive a network that drops idle connections - see Pool liveness.

Var Default Type Purpose
DATABASE_URL none - required when migrations exist String Connection URL. Scheme selects the driver: sqlite://path, postgres://... / postgresql://..., mysql://..., mariadb://.... The framework auto-creates the parent directory for SQLite paths. serve skips the database connection entirely when the configured Migrator has no migrations.
DB_MAX_CONNECTIONS 10 u32 sqlx pool ceiling.
DB_MIN_CONNECTIONS 1 u32 sqlx pool floor (kept warm).
DB_CONNECT_TIMEOUT 30 (seconds) u32 How long sqlx will wait for an initial connection before erroring.
DB_LOGGING false bool When true, sqlx logs every statement (use sparingly in production - chatty).
DB_IDLE_TIMEOUT unset (sqlx uses 600 seconds) u64 (seconds) How long a pooled connection may sit idle before the pool closes it. 0 disables idle reaping.
DB_MAX_LIFETIME unset (sqlx uses 1800 seconds) u64 (seconds) How long a pooled connection may live before the pool recycles it. 0 disables lifetime recycling.
DB_ACQUIRE_TIMEOUT unset (falls back to DB_CONNECT_TIMEOUT) u64 (seconds) How long a caller waits for a free pooled connection. Overrides DB_CONNECT_TIMEOUT for the checkout wait; set one or the other, not both. Zero is rejected at boot.
DB_TEST_BEFORE_ACQUIRE true bool Ping a pooled connection before handing it out. Leave it on unless you have measured the per-checkout round trip and DB_PING_AFTER_IDLE is not enough.
DB_PING_AFTER_IDLE unset u64 (seconds) Ping a pooled connection only after it has been idle this long. Setting it turns DB_TEST_BEFORE_ACQUIRE off, so hot connections are handed out untouched.
SUPRNOVA_AUTO_MIGRATE_BEST_EFFORT false bool When true, a failing auto-migration during serve boot is logged but does not abort. Default is fail-closed: boot exits non-zero rather than start against a partially-migrated schema. Pass --no-migrate to skip auto-migration entirely.

Session

Cookie attributes and lifetime for the session subsystem. Note that SESSION_SECURE defaults to true - production-safe by default; flip it off only for local HTTP development.

Var Default Type Purpose
SESSION_LIFETIME 120 (minutes) u64 Session lifetime in minutes. Parsed via env_optional; falls back silently if unparseable.
SESSION_TOUCH_INTERVAL 300 (seconds) u64 Minimum sliding-expiry persistence cadence. Runtime enforcement caps it at half the session lifetime.
SESSION_GC_INTERVAL 3600 (seconds) u64 Cadence for the supervised expired-session collector installed by SessionMiddleware::install.
SESSION_COOKIE "suprnova_session" String Session cookie name.
SESSION_PATH "/" String Cookie Path= attribute.
SESSION_DOMAIN unset String Cookie Domain= attribute. Leave unset for host-only cookies (the safer default for most apps).
SESSION_SECURE true bool Cookie Secure attribute. Defaults to true; set to false only in local HTTP development. cookie_http_only is always true and is not env-configurable.
SESSION_SAME_SITE "Lax" String SameSite attribute. Accepts Strict, Lax, None (case-insensitive).
SESSION_COOKIE_PREFIX unset String (__Host- / __Secure-) Prefix applied to session and remember-me wire names. Config::init validates the value and its SESSION_DOMAIN / SESSION_PATH constraints at boot; invalid combinations fail before serving.
SESSION_PARTITIONED false bool Emit the Partitioned / CHIPS cookie attribute for third-party-isolated cookies.
SESSION_EXPIRE_ON_CLOSE false bool When true, drop Max-Age so the browser deletes the cookie on close (session-cookie semantics).
SESSION_CONNECTION unset String Named DB connection for the session store. Unset means the default connection.
REMEMBER_LIFETIME 43200 (30 days, in minutes) u64 "Remember me" cookie/token lifetime in minutes.

Localization

The three APP_* variables the localization subsystem reads. Everything else about it - the detection chain, the session key and cookie name it consults, Unicode isolation marks - is code-level configuration on LocalizationConfig, not env. See Localization.

Var Default Type Purpose
APP_LOCALE "en" String (BCP-47) Locale used when the detection chain (session → cookie → Accept-Language) finds nothing. Also the locale suprnova generate-types extracts message keys from for lang-keys.ts. A value that is not a valid BCP-47 identifier fails boot rather than silently defaulting.
APP_FALLBACK_LOCALE "en" String (BCP-47) Locale consulted when a key is missing from the current locale's catalog. A key missing from both renders as the key itself plus a one-time warn!; Lang::try_get returns Err instead. Same strict parse as APP_LOCALE.
APP_LOCALE_PARENTS none - empty map String (comma-separated child=parent pairs, BCP-47 on each side) Per-locale fallback parents consulted before APP_FALLBACK_LOCALE, e.g. APP_LOCALE_PARENTS=pt-PT=pt-BR,en-AU=en-GB. Lang's fallback chain walks these transitively, and FluentTranslator flattens each locale's configured parent chain into its served catalog. A malformed pair, an invalid locale, a child named more than once, or a cycle (including a locale naming itself as its own parent) fails boot rather than degrading at request time. See Fallback chains.

Catalogs themselves are files, not env: lang/<locale>/*.ftl under APP_BASE_PATH. A missing lang/ directory is not an error - the app boots with the framework's embedded English validation catalog.

Cache

Var Default Type Purpose
CACHE_DRIVER memory String (memory/in-memory/inmemory, redis) Selects the bootstrap target. Memory keeps everything in-process; Redis requires REDIS_URL and fails boot if unreachable. Unknown values fail boot with a clear error.
REDIS_URL "redis://127.0.0.1:6379" String Redis connection URL (consulted only when CACHE_DRIVER=redis).
REDIS_PREFIX "suprnova_cache:" String Key prefix for cache entries (collision-avoidance for shared Redis).
CACHE_DEFAULT_TTL 3600 (seconds) u64 Default TTL in seconds. 0 means "no expiration". Applied to Cache::put(None) / Cache::tags_put(None); Cache::forever and Cache::remember_forever always bypass.
REDIS_COMMAND_RETRIES 0 u32 Extra retries for read-shaped Redis commands, on top of the one every read already gets. Applies to the cache, queue, and rate-limit drivers. Writes never retry at any value. Budget it in seconds: a retry against a dropped connection waits for the reconnect, so it costs the driver's whole connect and response budget - up to 3 connect retries at most 500 ms apart, each capped at 2 s, plus a 5 s response timeout on the cache driver; up to 6 connect retries with an uncapped exponential delay, each capped at 1 s, plus a 500 ms response timeout on the queue and rate-limit drivers. The clamp of 10 bounds attempts, not seconds: at that setting one read makes 12 attempts. A timeout counts as transient too, so during a stall each wrapped read issues up to that many commands. An unparseable value falls back to 0.

Queue

Var Default Type Purpose
QUEUE_DRIVER memory String (memory, sync, null, redis, database, failover) Active queue backend. sync runs each job inline when it is pushed, with no worker and no retry. null accepts every job and runs none. An unknown value is a boot error in production, where an in-memory queue would lose every job at the next restart. Elsewhere it logs a warn! that lists the accepted names and falls back to memory. failover wraps an ordered list of the others - see QUEUE_FAILOVER_CONNECTIONS.
QUEUE_CONNECTIONS unset String (comma-separated, e.g. redis,database) Registers one named connection per entry, next to the default connection. Each connection is named for its driver and reads that driver's own variables, exactly as it would if it were QUEUE_DRIVER. An entry accepts the same names as QUEUE_DRIVER. An unknown name is a boot error in every environment. An entry that names the driver QUEUE_DRIVER selects is a second name for the default connection. Any other entry over a Redis stream or a jobs table that another connection already uses is refused at boot, because one queue has one connection. A job, a route or a push selects a connection by name - see Queues.
QUEUE_AFTER_COMMIT false bool (true or 1 turns it on; anything else leaves it off) When on, every push made inside DB::transaction waits for the commit, whatever the job declares, as the after_commit option of a Laravel queue connection does. A rollback discards the push. It applies to jobs, queued mail and queued notifications. Read at each push, not once at boot.
QUEUE_PAUSABLE true bool (false or 0 turns it off) Whether workers and the queue:pause command honour pause signals. queue:resume never checks it, so an existing pause can always be cleared.
QUEUE_FAILOVER_CONNECTIONS - String (comma-separated, e.g. redis,database) Priority-ordered connection list for QUEUE_DRIVER=failover. Required when that driver is selected; a missing or blank value is a boot error, as is an entry naming failover (no nesting) or a driver that doesn't exist. Each entry reads its own driver's variables. Only pushes fall through the list; every read and every acknowledgement goes to the first connection, so each fallback needs its own worker.
QUEUE_REDIS_URL "redis://127.0.0.1:6379" String Redis URL (required-by-driver when QUEUE_DRIVER=redis).
QUEUE_REDIS_STREAM "suprnova-queue" String Redis Stream key used for fan-out.
QUEUE_REDIS_GROUP "default" String Consumer-group name.
QUEUE_REDIS_CONSUMER suprnova-<uuid> (a fresh UUID for each process) String Consumer name within the group. Set it per worker to keep the name stable across restarts.
QUEUE_VISIBILITY_TIMEOUT_SECS 60 u64 How long a claimed job stays invisible before another consumer can reclaim it. Match this to your slowest job.
QUEUE_DB_TABLE "jobs" String Table name for the database driver. Validated as a SQL identifier - a malformed value fails at boot, not at SQL composition time. Required-by-driver when QUEUE_DRIVER=database; the driver also requires DB::init() to have run first.
QUEUE_FAILED_DB_TABLE "failed_jobs" String Table the dead-letter store writes to. Bound automatically when QUEUE_DRIVER=database - the failed-job commands (queue:failed, queue:retry, queue:forget, queue:flush, queue:prune-failed) read it and Queue::retry_failed needs it, so the table is part of that driver's contract. The commands read no variable of their own: they boot the application as queue:work does and use the store that boot bound. With no store bound, they exit non-zero and say so. Not used by memory (ephemeral by construction) or redis (no table to write to). Unlike QUEUE_DB_TABLE a malformed identifier here does not fail boot: it logs at error! and leaves no store bound, so dead-lettered jobs are logged in full rather than persisted. Recoverable by hand, but not by the failed-job commands.

Schedule

Var Default Type Purpose
SCHEDULE_ALLOW_MEMORY_LOCK_IN_PRODUCTION unset bool-ish Acknowledges that a task marked on_one_server() is electing a leader through a per-process cache. That election is only as shared as the cache behind it, so in production CACHE_DRIVER=memory plus a single-server task is a hard boot failure naming the offending tasks, rather than a silent downgrade to "every replica runs it". Set this only where the deployment genuinely runs one scheduler; otherwise set CACHE_DRIVER=redis. See Scheduling.

Workflow

The #[workflow] long-running stateful worker. All values are clamped to safe minimums rather than honoured blindly - a WORKFLOW_CONCURRENCY=0 would park the worker semaphore forever, so the framework warns and clamps instead of accepting an obviously-broken config.

Var Default Type Purpose
WORKFLOW_CONCURRENCY 4 usize Maximum concurrent workflow executions per worker process. Clamped to >= 1.
WORKFLOW_POLL_INTERVAL_MS 1000 (ms) u64 How often the worker polls for newly-due workflows.
WORKFLOW_LOCK_TIMEOUT_SECS 30 (seconds) u64 Reclaim timeout for a claimed workflow row whose worker has died.
WORKFLOW_MAX_ATTEMPTS 3 i32 Max attempts per workflow run before it is marked failed. Clamped to >= 1.
WORKFLOW_RETRY_BACKOFF_SECS 5 i64 Linear backoff per attempt. Clamped to >= 0 - negative backoff would schedule retries in the past and produce a tight-loop reclaim.

Mail

MAIL_DRIVER defaults to log - outgoing mail prints to the configured tracing subscriber rather than reaching the network. Flip to memory in tests, file for .eml previews you can open in a mail client, and smtp/ses/etc. in production. The provider-specific keys/tokens are required only when that driver is selected; an unknown driver value logs a warn! and falls back to log.

Var Default Type Purpose
MAIL_DRIVER "log" String (log, memory, file, smtp, ses, sendgrid, mailgun, postmark, resend) Selects the bootstrap target.
MAIL_FROM none - required by auth-flow facades String Default from-address for auth-flow facades (EmailVerification, PasswordReset, TwoFactor). Required for those paths; absent it errors at the call site rather than silently falling back to a placeholder that would break DMARC/SPF.
MAIL_FROM_NAME unset String Optional display name for the auth-flow From (since 0.5.9). When set, the header renders Name <MAIL_FROM>; MAIL_FROM stays a bare address. Read at send time, so it applies to queued auth-flow mail too.

File (MAIL_DRIVER=file)

Var Default Type Purpose
MAIL_FILE_PATH storage_path("mail") String Directory one RFC 5322 .eml file is written to per send. Never pruned. Absolute paths are used as given; relative paths anchor at the application base directory (see APP_BASE_PATH).

SMTP (MAIL_DRIVER=smtp)

Var Default Type Purpose
MAIL_SMTP_HOST "127.0.0.1" String SMTP host.
MAIL_SMTP_PORT 587 u16 SMTP port.
MAIL_SMTP_USER unset String SMTP username. Both MAIL_SMTP_USER and MAIL_SMTP_PASS must be set for an encrypted transport; with neither, the connection defaults to the unencrypted local-catcher mode. Setting exactly one warns at boot.
MAIL_SMTP_PASS unset String SMTP password. See MAIL_SMTP_USER for the partial-credentials behaviour.
MAIL_SMTP_ENCRYPTION derived starttls | tls | none How the connection is encrypted. Unset derives from the credentials: starttls when both are set, none when neither is. tls selects implicit TLS (port 465). ssl and null are accepted as Laravel-compatible aliases. An unrecognised value fails boot in every environment - a typo must not degrade to cleartext.
MAIL_ALLOW_INSECURE_SMTP_IN_PRODUCTION unset bool-ish Production refuses to boot on an unencrypted SMTP connection. Set to 1/true/yes/on to acknowledge cleartext - defensible only when the relay is reachable solely over a private network.

Postmark (MAIL_DRIVER=postmark)

Var Default Type Purpose
MAIL_POSTMARK_TOKEN required-by-driver String Postmark server token.
MAIL_POSTMARK_ENDPOINT Postmark default String Override the API endpoint (regional or mock server).

Amazon SES (MAIL_DRIVER=ses)

Var Default Type Purpose
MAIL_SES_ACCESS_KEY required-by-driver String AWS access key.
MAIL_SES_SECRET_KEY required-by-driver String AWS secret key.
MAIL_SES_REGION "us-east-1" String AWS region.
MAIL_SES_ENDPOINT AWS default for the region String Override the SES endpoint (regional or mock server).

SendGrid (MAIL_DRIVER=sendgrid)

Var Default Type Purpose
MAIL_SENDGRID_API_KEY required-by-driver String SendGrid API key.
MAIL_SENDGRID_ENDPOINT SendGrid default String Override the API endpoint.

Mailgun (MAIL_DRIVER=mailgun)

Var Default Type Purpose
MAIL_MAILGUN_API_KEY required-by-driver String Mailgun API key.
MAIL_MAILGUN_DOMAIN required-by-driver String Mailgun sending domain.
MAIL_MAILGUN_ENDPOINT Mailgun default String Override the API endpoint (e.g. EU vs US).

Resend (MAIL_DRIVER=resend)

Var Default Type Purpose
MAIL_RESEND_API_KEY required-by-driver String Resend API key.
MAIL_RESEND_ENDPOINT Resend default String Override the API endpoint.

Rate Limiting

Var Default Type Purpose
RATE_LIMIT_DRIVER memory String (memory, redis) Selects the rate-limiter backend. Outside production an unknown value logs a warn! and falls back to memory; in production, memory - including via an unknown value - fails boot unless RATE_LIMIT_ALLOW_MEMORY_IN_PRODUCTION is set.
RATE_LIMIT_ALLOW_MEMORY_IN_PRODUCTION unset bool-ish Acknowledges per-process rate-limit buckets in production. Only accurate if you run exactly one process: behind N replicas every quota is effectively N× and resets on each deploy.
RATE_LIMIT_REDIS_URL "redis://127.0.0.1:6379" String Redis URL (required-by-driver when RATE_LIMIT_DRIVER=redis).
RATE_LIMIT_PREFIX "suprnova:" String Key prefix in Redis.

Filesystem

When S3_BUCKET is set, the server registers an S3 disk named s3 as it boots, beside the queue, the rate limiter and the mail transport. The variables work for AWS S3 and for S3-compatible services (MinIO, RustFS, R2, B2). With S3_BUCKET unset, nothing is registered. A disk that your bootstrap() registered under the name s3 is kept as it is, and the variables are not read then.

Var Default Type Purpose
S3_BUCKET unset (no disk) String The bucket. Setting it registers the s3 disk.
S3_REGION none - see AWS_REGION String The region. The driver needs one, and the server does not boot when none is set. A service that is not AWS takes any name, such as us-east-1 or auto.
AWS_REGION, AWS_DEFAULT_REGION unset String Read, in this order, when S3_REGION is not set.
S3_ENDPOINT unset (AWS) String The endpoint of a service that is not AWS.
S3_ACCESS_KEY, S3_SECRET_KEY unset String The access key and the secret. Set both or neither. One without the other fails boot. With neither, the driver uses the default credential chain of AWS: AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, the profile, and the role of the instance.
S3_ROOT unset String A prefix inside the bucket that every path is under.
S3_PUBLIC_URL unset String The public base URL of the s3 disk, which Storage::url("s3", path) joins with the path. It is an absolute http or https URL (https://cdn.example.com/files) or a path of your own host (/storage). A value with a user or a password, a query or a fragment, a backslash, or a . or .. segment fails boot. With it unset, Storage::url on the s3 disk returns an error.

Blank values count as unset. No error repeats the value of a variable.

There is no default disk. Every call names the disk it uses, so FILESYSTEM_DISK is not read. The console binary boots no driver of the environment. A command that uses the s3 disk calls suprnova::filesystem::bootstrap_from_env() in its own bootstrap. To register the same configuration under a name of your own, use S3Config::from_env(). See Filesystem.

The three vector drivers read their connection from the environment when you call their from_env() constructor. Nothing reads these variables at boot. You choose the driver in Rust and register it in bootstrap(). See Vector.

Var Default Type Purpose
QDRANT_URL none - required String The gRPC URL of Qdrant, port 6334 by default. QdrantVectorDriver::from_env() fails when it is unset or blank.
QDRANT_API_KEY unset String The key of Qdrant Cloud, or of a self-hosted instance that asks for one.
MARIADB_URL none - see DATABASE_URL String The URL of the MariaDB that holds the vectors. MariaDbVectorDriver::from_env() reads it first.
DATABASE_URL none String Read by MariaDbVectorDriver::from_env() when MARIADB_URL is unset, and taken only when its scheme is mariadb:// or mysql://. A URL of another engine is not taken.
PINECONE_API_KEY none - required String The Pinecone API key. PineconeVectorDriver::from_env() fails when it is unset or blank.
PINECONE_CONTROLLER_HOST https://api.pinecone.io String The base URL of the Pinecone control plane.
PINECONE_API_VERSION 2025-04 String The value of the X-Pinecone-Api-Version header.

A missing variable is named in the error at boot, and the error never shows a URL, because a URL carries the password.

Images

Image driver selection and the decode limits that bound hostile input. Out-of-range limits clamp with a warn! rather than failing boot: a limit of zero would reject every image in the application. An unknown IMAGE_DRIVER fails at first use, naming the valid values.

Var Default Type Purpose
IMAGE_DRIVER oxideav String (oxideav, magick) Selects the image backend. oxideav is pure Rust with no host dependency; magick shells out to a host-installed ImageMagick 7 for wider input support. Case-insensitive.
IMAGE_MAX_DIMENSION 16384 u32 Cap on the width and height of a decoded image, checked against the input's own header before anything is allocated. Also caps resize targets. Minimum 1.
IMAGE_MAX_ALLOC_BYTES 268435456 (256 MiB) u64 Cap on the decoded RGBA footprint (width * height * 4). Also caps the size of the source file itself, whether it arrives from a path, a disk, or Image::from_stream (which checks while collecting). Minimum 4.
IMAGE_MAGICK_BINARY magick String Binary the magick driver invokes. ImageMagick 7 only; the ImageMagick 6 convert name is not accepted. A missing binary is a clear error at first use.
IMAGE_MAGICK_TIMEOUT_SECS 30 u32 Wall-clock ceiling on a single ImageMagick invocation. It is both ImageMagick's own -limit time argument and the Rust-side deadline that kills the child's whole process group two seconds later, because -limit time is enforced by a monitor that a child wedged inside a delegate never engages. Bounds a stalled delegate that would otherwise hold a blocking worker for the life of the process. magick driver only. Minimum 1.

See Images for the two-tier limit enforcement and how to choose between the drivers.

Hashing

Password-hashing driver and per-algorithm parameters. Invalid values return a FrameworkError::param at first hash, surfacing misconfiguration immediately instead of silently defaulting.

Var Default Type Purpose
HASH_DRIVER bcrypt String (bcrypt, argon/argon2i, argon2id) Active hashing algorithm. Case-insensitive.
HASH_ROUNDS 12 u32 Bcrypt cost (range 4..=31). Out-of-range values fail with a clear error.
HASH_MEMORY 65536 (64 MiB, KiB units) u32 Argon2 memory in KiB. Minimum 8. Argon-only.
HASH_TIME 4 u32 Argon2 time / iterations. Minimum 1. Argon-only.
HASH_THREADS 1 u32 Argon2 parallelism (matches OWASP / libsodium). Minimum 1. Argon-only.
HASH_VERIFY false bool When true, verify() rejects hashes from a different algorithm than HASH_DRIVER (returns Ok(false)). Default false so legacy bcrypt hashes still verify after a driver flip until they're rotated.

Validation

Var Default Type Purpose
HIBP_TIMEOUT_SECS 30 (seconds) u64 Request timeout for Password::uncompromised()'s Have I Been Pwned range check, read fresh each time a default HibpVerifier is constructed. A slow or unreachable HIBP still fails open - see Validation.

Auth Flows

Two-factor authentication uses APP_NAME (covered under Application) as the TOTP issuer string - there is no dedicated 2FA_ISSUER env var. The issuer falls back to "Suprnova" when APP_NAME is unset.

Inertia / Frontend

Var Default Type Purpose
SUPRNOVA_FRONTEND svelte String (svelte, react, vue) Active frontend. Case-insensitive. Drives Frontend::detect_from_env(), the default Vite entry point, and the page-component extension search order at compile time. Unknown or unset values fall back to svelte.

Maintenance Mode

Var Default Type Purpose
MAINTENANCE_DRIVER file String (file, cache) Selects how down/up state is stored. file writes to the framework storage path; cache rides on the configured cache driver (useful when many app instances must coordinate maintenance state). Any other value falls back to file.

Events

Var Default Type Purpose
EVENT_MAX_CONCURRENCY 256 usize Ceiling on concurrent queued listener tasks. Values <= 0 or unparseable fall back to the default. Applies to Event::queue / queued listeners; sync listeners are not subject to this limit.

Logging

LOG_FORMAT is environment-aware: in production (APP_ENV=production) the default is json for log-aggregator friendliness; everywhere else the default is pretty for human-readable local/dev output. An explicit value always wins.

Var Default Type Purpose
LOG_LEVEL "info" String (error, warn, info, debug, trace - case-insensitive) Tracing-subscriber filter level.
LOG_FORMAT env-aware (json in production, pretty elsewhere) String (json, pretty) Tracing-subscriber output format.

Observability (OpenTelemetry)

Var Default Type Purpose
OTEL_EXPORTER_OTLP_ENDPOINT unset (telemetry disabled) String Base URL of the OTLP collector, for example http://localhost:4318. Each signal is sent to its own path under it: /v1/traces, /v1/metrics, /v1/logs. A trailing slash is trimmed. When unset (or whitespace), exporters are not installed and the framework keeps using the standard tracing subscriber. This variable is what turns telemetry on; the three per-signal variables below have no effect without it.
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT unset String The URL the traces are sent to, used as it is written, with no path added. Unset sends traces to /v1/traces under the base endpoint.
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT unset String The URL the metrics are sent to, used as it is written. Unset sends metrics to /v1/metrics under the base endpoint.
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT unset String The URL the logs are sent to, used as it is written. Unset sends logs to /v1/logs under the base endpoint.
OTEL_EXPORTER_OTLP_COMPRESSION unset (no compression) String (gzip) Read by the OpenTelemetry exporter, not by Suprnova. gzip works. zstd is not compiled in: asking for it leaves the signals out, with the reason in the log.
OTEL_SERVICE_NAME "suprnova" String service.name resource attribute on every span / metric / log record.
OTEL_SERVICE_VERSION CARGO_PKG_VERSION at build time String service.version resource attribute.
OTEL_SDK_DISABLED false bool Standard OTel kill switch. When true, exporters are not installed regardless of OTEL_EXPORTER_OTLP_ENDPOINT.

An endpoint must be a URL with the scheme http or https and a host. A signal whose endpoint is not one, such as a path alone, is left out and reported in the log, and the other signals are still exported. The report never repeats the endpoint, which can carry a password or a token. The exporters need the otel cargo feature. The other standard OTLP variables (OTEL_EXPORTER_OTLP_HEADERS, _PROTOCOL, _TIMEOUT) are read by the OpenTelemetry exporter directly.

CLI / dev server

These are read by the suprnova CLI binary (dev server, SSR worker) rather than the runtime framework - they appear in the starter .env or are honoured by suprnova serve / suprnova ssr:*.

Var Default Type Purpose
VITE_PORT 5765 u16 Port Vite binds to in suprnova serve. CLI --frontend-port overrides.
SUPRNOVA_SSR_RUNTIME "node" String Runtime to launch the SSR worker under (suprnova ssr:start). CLI --runtime overrides.
SUPRNOVA_SSR_BUNDLE frontend/bootstrap/ssr/ssr.js Path Path to the built SSR bundle. CLI --bundle overrides.
SUPRNOVA_SSR_URL "http://127.0.0.1:13714" String SSR worker URL for suprnova ssr:check. CLI --url overrides.

Subsystems with no env vars

A few subsystems are configured entirely in Rust code via the container or service registration - they have zero env vars the framework reads:

  • Filesystem / Storage, apart from S3. Disks are registered by name with Storage::register_* in bootstrap() and addressed by name at the call site (Storage::disk("public")). There is no default disk, so there is no FILESYSTEM_DISK env var (see "Variables the framework does not read" below). The one exception is the S3-compatible s3 disk, which the S3_* variables configure (see Filesystem above and Docker).
  • Broadcasting & WebSockets. Channels are registered with the ws!() macro and BroadcastHub configuration in code. The driver itself rides on whatever the configured CACHE_DRIVER selects.
  • CORS, CSRF, Idempotency, Timeout. Configured via builder structs passed to the middleware constructors in bootstrap(). The defaults are conservative enough that a typical app never touches them.
  • Magnetar and OAuth. MagnetarConfig is built in application bootstrap. The API starter reads PASSKEY_RP_ID and PASSKEY_RP_ORIGIN, but the framework itself does not. OAuth provider IDs, secrets, callback URLs, scopes, transports, and policy values are supplied programmatically through the Magnetar provider registry. Applications may source those values from environment variables or a secret manager.
  • Notifications, Payments, Feature Flags. Each registers concrete drivers via App::bind in bootstrap(). Pick your driver in Rust; pass any URLs/keys it needs as your own env vars.
  • Vector search registers its drivers the same way. The Qdrant, MariaDB and Pinecone drivers have a from_env() constructor that reads the variables in Vector search above.

Variables the framework does not read

The scaffolded starter .env lists a few keys for human-author convenience that the framework never consults. They're documented here so a reader searching for them isn't left wondering:

  • MAIL_FROM_ADDRESS - a Laravel-style placeholder the framework never consults. The actual from-address the auth-flow facades use is MAIL_FROM (covered under Mail). Your own Mailable types can read it via env_optional if you want to keep the Laravel name, but nothing in suprnova::* does. (MAIL_FROM_NAME is read as of 0.5.9 - see the Mail chapter - so it's no longer listed here.)
  • FILESYSTEM_DISK - Laravel's default disk name. Suprnova has no default disk: every call names the disk it uses (Storage::disk("s3")).

How values are parsed

A short reference for the three env-helper variants - see Configuration for the full treatment:

Helper Behaviour on missing Behaviour on unparseable
env(key, default) returns default warn! + returns default
env_required(key) panics panics
env_optional(key) returns None warn! + returns None
env_strict(key) (internal, used by try_from_env) returns Ok(None) returns Err(FrameworkError) - boot aborts

Strict variants (AppConfig::try_from_env, ServerConfig::try_from_env) are what Config::init calls, so a typo in APP_DEBUG=tru or SERVER_PORT=80a0 aborts boot with a structured error instead of silently reverting to the default. Lenient variants exist for the broader call-site population (including impl Default) where a parse failure must not panic.

Per-environment overrides

The loader reads files in this order, each overriding the previous:

  1. .env
  2. .env.<environment> (e.g. .env.production, .env.staging, .env.testing, .env.<custom> for APP_ENV=<custom>)
  3. Process environment

That means a containerised production deploy can ship a minimal .env.production overriding only the keys that differ from .env (driver names, URLs, key material), and the real container env overrides both for secrets that should never land in a committed file.

See Configuration for the exact loader behaviour and the tracking of loaded keys that prevents stale .env values from promoting into the "real system env" tier across reloads.

Next