Skip to content

Environment variables

Every variable the template ships, what reads it, and what it does when you leave it alone. Configuration covers the mechanism — Env, the config/ directory, dot notation. This is the list.

Two rules run through all of it. A variable is only read by the config file that names it, so a variable no config/*.ts mentions does nothing — a template test holds those two files together. And Env treats an empty value as absent, so DB_PASSWORD= and a missing DB_PASSWORD behave the same way.

Application

VariableDefault
APP_NAMEElvelUsed in mail subjects and the default from name
APP_ENVlocalproduction turns on the auth rate limit and turns off debug pages
APP_DEBUGtrueStack traces in responses. Never true in production
APP_URLhttp://localhost:3000Absolute URLs, signed links, trusted origins
APP_TIMEZONEUTC
APP_KEY(empty)Signs cookies and encrypts. elvel key:generate writes it
APP_PREVIOUS_KEYS(empty)Comma-separated retired keys, still able to read what they wrote
PORT3000
HOST(empty)Empty binds every interface

APP_KEY ships empty on purpose: a default in the framework's own repository would be a shared secret. Boot fails loudly instead.

Database

Read only once @elvel/database is registered in bootstrap/providers.ts.

VariableDefault
DB_CONNECTIONsqlitesqlite, postgres or mysql
DB_URL(empty)A full DSN, which wins over the host/port pieces
DB_DATABASEdatabase/database.sqliteA path for sqlite; a name otherwise
DB_HOST127.0.0.1
DB_PORT5432 postgres, 3306 mysqlEach connection carries its own default
DB_USERNAMEpostgres / rootPer connection, as above
DB_PASSWORD(empty)
DB_POOL_MAX10Connections per pool
DB_CONNECT_TIMEOUT30Seconds
DB_FOREIGN_KEYStruesqlite only, where they are off unless asked for
DB_READ_HOST / DB_WRITE_HOST127.0.0.1Split reads and writes

Session and CSRF

VariableDefault
SESSION_ENABLEDtrueOff leaves cookies and CSRF without a session
SESSION_DRIVERfilememory, file, database, redis or cache
SESSION_COOKIEelvel_session
SESSION_LIFETIME7200Seconds
SESSION_SAME_SITElaxOr strict
SESSION_SECURE(unset)Unset means on in production
SESSION_ENCRYPTfalseEncrypt the cookie, not only sign it. Needs @elvel/encryption
SESSION_TABLEsessionsThe database driver's table
SESSION_STORE(empty)Cache store for the redis/cache drivers
SESSION_CSRFtrue

Cache, queue and Redis

VariableDefault
CACHE_STOREfilearray, file, database or redis
CACHE_PREFIXelvel_cache_Keeps two applications on one Redis apart
CACHE_MEMORY0Seconds a value may be served from this process without rereading the store. 0 is off, and is right for counters
CACHE_LIMITERarrayStore the rate limiter counts in
QUEUE_CONNECTIONsyncsync, database or redis
QUEUE_FAILED_DRIVERnulldatabase to keep failed jobs
REDIS_URLredis://127.0.0.1:6379Read by the redis cache store and queue only

Mail

VariableDefault
MAIL_MAILERloglog, array, smtp, resend or failover
MAIL_FROM_ADDRESShello@example.com
MAIL_FROM_NAME(the app name)
MAIL_ALWAYS_TO(empty)Redirect every message here — for a staging box
MAIL_HOST127.0.0.1
MAIL_PORT1025Mailpit's default
MAIL_USERNAME / MAIL_PASSWORD(empty)
MAIL_ALLOW_SELF_SIGNEDfalseFor a local SMTP box with its own certificate
RESEND_KEY(empty)The resend mailer

Storage

VariableDefault
FILESYSTEM_DISKlocal
S3_BUCKET(empty)
S3_KEY / S3_SECRET(empty)
S3_REGIONus-east-1
S3_ENDPOINT(empty)Set it for R2, MinIO or Spaces
S3_PREFIX(empty)Share one bucket between applications

Auth

VariableDefault
AUTH_SECRET(empty)Signs better-auth's tokens. Never reuse APP_KEY
AUTH_MOUNTtrueOff leaves the Gate without the endpoints

Empty is worse than wrong here — better-auth signs with an empty string and says nothing — so elvel auth:secret generates one rather than a default being shipped.

HTTP, CORS and security headers

VariableDefault
TRUSTED_PROXIES(empty)Comma-separated. Empty trusts none
HTTP_CHECK_PORTtrueRefuse to start on a port somebody else holds
CORS_ORIGINS*Comma-separated. Name them before turning credentials on
CORS_CREDENTIALSfalse
SECURITY_HEADERStrue
SECURITY_CSP_REPORT_ONLYfalseReport violations without enforcing

TRUSTED_PROXIES decides whether X-Forwarded-* is believed, which the auth rate limit depends on — see the address it counts. Name both loopback forms if that is where your proxy sits: 127.0.0.1 does not cover ::1.

Logging

VariableDefault
LOG_CHANNELstack
LOG_LEVELdebuginfo on the production channel
LOG_REQUESTSfalseOne line per request

Your own

config/services.ts is where third-party credentials go, so they are read once at boot and reachable by name rather than through process.env at the call site:

ts
// config/services.ts
export default {
  stripe: {
    key: env('STRIPE_KEY', ''),
    secret: env('STRIPE_SECRET', ''),
    webhookSecret: env('STRIPE_WEBHOOK_SECRET', '')
  }
}
ts
config('services.stripe.key')

MIT. Alpha — the shape is settled, the surface still moves.