Configuration
This page is a dictionary of every key Loco’s configuration loader understands. It documents struct Config (src/config/mod.rs) and its sub-structs (src/config/{auth,server,database,logger,mailer,queue,cache}.rs). For a narrative walkthrough of settings and environments, see the-app/your-project.
Loading & precedence
-
Default config folder:
config/(Config::new,src/config/mod.rs:128-131). Override with theLOCO_CONFIG_FOLDERenv var (read byEnvironment::load,src/environment.rs:59-64). -
Config::from_folder(env, path)(src/config/mod.rs:153-174) resolves the file to load with this precedence — first file that exists wins:{path}/{env}.local.yaml{path}/{env}.yaml
If neither exists, loading fails with
Error::Message("no configuration file found in folder: ..."). -
Before parsing, the entire YAML file is rendered as a Tera template (
src/config/mod.rs:170,src/tera.rs:5-8,Tera::one_off(.., autoescape=false)).get_env(name=.., default=..)used throughout the shipped config files is Tera’s own built-in function — it is not a Loco-registered function. -
Parse failures raise
Error::YAMLFile(err, path)(src/config/mod.rs:172-173). -
ConfigimplementsDisplayby dumping itself back to YAML (src/config/mod.rs:191-196). -
Config::get_jwt_config(&self) -> Result<&JWT>(src/config/mod.rs:180-188) returns an error ifauthorauth.jwtis absent.
Environment resolution
environment::resolve_from_env() (src/environment.rs:32-38) picks the active environment name with this precedence:
LOCO_ENVRAILS_ENVNODE_ENV- fallback:
"development"(DEFAULT_ENVIRONMENT,src/environment.rs:21)
Top-level Config
struct Config — src/config/mod.rs:62-92. Every field is a top-level YAML key.
| Key | Type | Required? | Notes |
|---|---|---|---|
logger | Logger | required | mod.rs:64 |
server | Server | required | mod.rs:65 |
database | Database | required, only when the with-db feature is enabled | #[cfg(feature = "with-db")], mod.rs:66-67 |
cache | CacheConfig | optional — #[serde(default)], defaults to Null | mod.rs:68-69 |
queue | Option<QueueConfig> | optional | mod.rs:70 |
auth | Option<Auth> | optional | mod.rs:71 |
workers | Workers | optional — #[serde(default)] | mod.rs:72-73 |
mailer | Option<Mailer> | optional | mod.rs:74 |
initializers | Option<Initializers> (= Option<BTreeMap<String, serde_json::Value>>) | optional | mod.rs:75, type alias at mod.rs:106 |
settings | Option<serde_json::Value> | optional — #[serde(default)] | mod.rs:88-89; free-form app settings, surfaced at ctx.config.settings |
scheduler | Option<scheduler::Config> | optional | mod.rs:91; struct owned by the scheduler area, not detailed here |
auth
struct Auth — src/config/auth.rs:13-17.
auth: jwt: location: # optional, default: Bearer from: Bearer # or: {from: Query, name: <string>} / {from: Cookie, name: <string>} secret: <base64 secret> # required — must be valid base64 expiration: 604800 # required, u64 seconds (e.g. 7 days)| Key | Type | Required? | Notes |
|---|---|---|---|
auth.jwt | Option<JWT> | optional | auth.rs:16 |
auth.jwt.location | Option<JWTLocationConfig> | optional, default: Bearer (resolved by get_jwt_locations, src/controller/extractor/auth.rs:181-189) | auth.rs:24 |
auth.jwt.secret | String | required | auth.rs:26. Must be valid base64 — the JWT signer/verifier call EncodingKey/DecodingKey::from_base64_secret (src/auth/jwt.rs:83,108); a non-base64 string fails at token generation/validation time, not at config load time |
auth.jwt.expiration | u64 (seconds) | required | auth.rs:28 |
JWTLocationConfig (#[serde(untagged)], auth.rs:47-54) accepts either form:
Single(JWTLocation)— a single location mapMultiple(Vec<JWTLocation>)— a YAML list of location maps, tried in order until one yields a token
JWTLocation (#[serde(tag = "from")], auth.rs:35-44):
| Variant | YAML shape | Notes |
|---|---|---|
Bearer | from: Bearer | reads the Authorization: Bearer <token> header |
Query { name } | from: Queryname: <string> | reads a query-string parameter |
Cookie { name } | from: Cookiename: <string> | reads a cookie |
Related, not YAML-configurable: default signing algorithm is HS512 (JWT_ALGORITHM, src/auth/jwt.rs:13), overridable in code via JWT::algorithm(..) (jwt.rs:51), not via config.
server
struct Server — src/config/server.rs:29-45.
server: binding: localhost # optional, default "localhost" port: 5150 # required host: http://localhost # required ident: <string> # optional — overrides the `Server` response header middlewares: {} # optional, default {} — see the middleware catalog reference| Key | Type | Required? | Notes |
|---|---|---|---|
server.binding | String | optional — #[serde(default = "default_binding")] → "localhost" | server.rs:33-34,47-49 |
server.port | i32 | required | server.rs:36 |
server.host | String | required | server.rs:38 |
server.ident | Option<String> | optional | server.rs:40. When set, replaces the Server header value |
server.middlewares | middleware::Config | optional — #[serde(default)] | server.rs:44. Struct is owned by the middleware area; see the middleware catalog reference for every middleware’s keys |
Server::full_url() -> String returns "{host}:{port}" (server.rs:52-55).
workers
struct Workers — src/config/server.rs:64-68.
workers: mode: BackgroundQueue # optional, default BackgroundQueue| Key | Type | Required? | Notes |
|---|---|---|---|
workers.mode | WorkerMode | optional — Workers derives Default | server.rs:67 |
WorkerMode (server.rs:71-83):
| Variant | Default? | Notes |
|---|---|---|
BackgroundQueue | yes | Workers run asynchronously via a queue backend. Requires a configured queue |
ForegroundBlocking | no | Workers run in-process and block the caller until the task completes |
BackgroundAsync | no | Workers run asynchronously in-process (async task, no external queue) |
database
struct Database — src/config/database.rs:22-84. Only present/required when the with-db feature is enabled.
database: uri: postgres://root:12341234@localhost:5432/myapp_development # required enable_logging: true # required — SQLx statement logging min_connections: 1 # required max_connections: 1 # required connect_timeout: 500 # required, milliseconds idle_timeout: 500 # required, milliseconds acquire_timeout: 500 # optional, milliseconds auto_migrate: true # optional, default false dangerously_truncate: false# optional, default false dangerously_recreate: false# optional, default false run_on_start: <sql/pragma> # optional| Key | Type | Required? | Notes |
|---|---|---|---|
database.uri | String | required | database.rs:28. E.g. postgres://... or sqlite://db.sqlite?mode=rwc |
database.enable_logging | bool | required | database.rs:31 — enables SQLx statement logging |
database.min_connections | u32 | required | database.rs:34 |
database.max_connections | u32 | required | database.rs:37 |
database.connect_timeout | u64 (ms) | required | database.rs:40 |
database.idle_timeout | u64 (ms) | required | database.rs:43 |
database.acquire_timeout | Option<u64> (ms) | optional | database.rs:46 |
database.auto_migrate | bool | optional — #[serde(default)] | database.rs:51-52. Runs pending migrations on boot; recommended for development, discouraged in production |
database.dangerously_truncate | bool | optional — #[serde(default)] | database.rs:56-57. Deletes row data on boot; typically used in test |
database.dangerously_recreate | bool | optional — #[serde(default)] | database.rs:63-64. Drops and recreates schema on boot |
database.run_on_start | Option<String> | optional | database.rs:83. Arbitrary SQL/PRAGMA statements executed once the connection is established. For SQLite, if unset, Loco applies its own PRAGMA defaults (foreign_keys=ON, journal_mode=WAL, synchronous=NORMAL, mmap_size=134217728, journal_size_limit=67108864, cache_size=2000, busy_timeout=5000) |
Note: the db_min_conn()=1 / db_max_conn()=20 / db_connect_timeout()=500 / db_idle_timeout()=500 helper functions in database.rs:86-100 are not defaults for Database itself (whose numeric fields have no #[serde(default)] and are required) — they are reused as the #[serde(default = ...)] values for the Postgres/Sqlite queue configs below.
logger
struct Logger — src/config/logger.rs:21-49.
logger: enable: true # required pretty_backtrace: false # optional, default false level: debug # required — off|trace|debug|info|warn|error format: compact # required — compact|pretty|json override_filter: <str> # optional — EnvFilter directive string file_appender: # optional enable: true # required within block non_blocking: false # optional, default false, within block level: info # required within block format: json # required within block rotation: daily # required within block — minutely|hourly|daily|never dir: ./logs # optional, default "./logs" filename_prefix: <s> # optional filename_suffix: <s> # optional max_log_files: 7 # required within block| Key | Type | Required? | Notes |
|---|---|---|---|
logger.enable | bool | required | logger.rs:24 |
logger.pretty_backtrace | bool | optional — #[serde(default)] | logger.rs:28-29. When true, forces nicely-formatted backtraces (development-friendly); turn off in performance-sensitive production deployments |
logger.level | logger::LogLevel | required | logger.rs:34. Variants: off, trace, debug, info (enum’s own #[default], but the field itself has no #[serde(default)] so it must be present in YAML), warn, error (src/logger.rs:15-36) |
logger.format | logger::Format | required | logger.rs:39. Variants: compact (#[default]), pretty, json (src/logger.rs:39-48) |
logger.override_filter | Option<String> | optional | logger.rs:45. A tracing-subscriber EnvFilter directive string |
logger.file_appender | Option<LoggerFileAppender> | optional | logger.rs:48 |
logger.file_appender.enable | bool | required within block | logger.rs:54 |
logger.file_appender.non_blocking | bool | optional — #[serde(default)] | logger.rs:57-58 |
logger.file_appender.level | logger::LogLevel | required within block | logger.rs:63 |
logger.file_appender.format | logger::Format | required within block | logger.rs:68 |
logger.file_appender.rotation | logger::Rotation | required within block | logger.rs:71. Variants: minutely, hourly (#[default]), daily, never (src/logger.rs:51-62) |
logger.file_appender.dir | Option<String> | optional, defaults to "./logs" when unset (applied at file-appender init, src/logger.rs:113) | logger.rs:76 |
logger.file_appender.filename_prefix | Option<String> | optional | logger.rs:79 |
logger.file_appender.filename_suffix | Option<String> | optional | logger.rs:82 |
logger.file_appender.max_log_files | usize | required within block | logger.rs:85 |
mailer
struct Mailer — src/config/mailer.rs:29-35.
# development: capture instead of sendingmailer: stub: false # optional, default false smtp: enable: true # required host: localhost # required port: 1025 # required secure: false # required — legacy shorthand, see below
# production: implicit TLS on port 465 (SMTPS)mailer: smtp: enable: true host: smtp.example.com port: 465 tls: implicit # overrides `secure`; see below auth: user: postmaster@mg.example.com password: "{{ get_env(name='SMTP_PASSWORD') }}" hello_name: <string> # optional — EHLO client id| Key | Type | Required? | Notes |
|---|---|---|---|
mailer.stub | bool | optional — #[serde(default)] | mailer.rs:33-34. When true, mail is captured rather than sent |
mailer.smtp | Option<SmtpMailer> | optional | mailer.rs:31 |
mailer.smtp.enable | bool | required | mailer.rs:55 |
mailer.smtp.host | String | required | mailer.rs:57 |
mailer.smtp.port | u16 | required | mailer.rs:59 |
mailer.smtp.secure | bool | required | mailer.rs:65. Legacy shorthand: true selects STARTTLS (port 587), false selects cleartext |
mailer.smtp.tls | Option<MailerTls> | optional — #[serde(default)] | mailer.rs:69. When set, overrides secure. Variants (#[serde(rename_all = "lowercase")], mailer.rs:38-50): starttls (opportunistic TLS, port 587 — what secure: true selects), implicit (TLS from the first byte, SMTPS, port 465 — required by providers that only accept implicit TLS), none (cleartext) |
mailer.smtp.auth | Option<MailerAuth> | optional | mailer.rs:71 |
mailer.smtp.auth.user | String | required within block | mailer.rs:93 |
mailer.smtp.auth.password | String | required within block | mailer.rs:95 |
mailer.smtp.hello_name | Option<String> | optional | mailer.rs:73. EHLO client identifier, sent instead of the hostname |
Effective TLS mode is resolved by SmtpMailer::tls_mode() (mailer.rs:76-87): if tls is set, it wins outright; otherwise secure: true → Starttls, secure: false → None.
queue
enum QueueConfig — src/config/queue.rs:5-14, #[serde(tag = "kind")] with variants Redis, Postgres, Sqlite.
# kind: Redisqueue: kind: Redis uri: redis://127.0.0.1 # required dangerously_flush: false # optional, default false queues: [high, low] # optional — priority order, first = most important num_workers: 2 # optional, default 2 # reaper: # optional, disabled by default (opt-in) # age_minutes: 10 # requeue jobs stuck in `processing` for longer than this # interval_seconds: 60 # optional, default 60 — how often to sweep
# kind: Postgresqueue: kind: Postgres uri: postgres://... # required dangerously_flush: false # optional, default false enable_logging: false # optional, default false max_connections: 20 # optional, default 20 min_connections: 1 # optional, default 1 connect_timeout: 500 # optional, default 500 (ms) idle_timeout: 500 # optional, default 500 (ms) poll_interval_sec: 1 # optional, default 1 num_workers: 2 # optional, default 2 # reaper: # optional, disabled by default (opt-in) # age_minutes: 10 # requeue jobs stuck in `processing` for longer than this # interval_seconds: 60 # optional, default 60 — how often to sweep
# kind: Sqlite (same shape as Postgres)queue: kind: Sqlite uri: sqlite://... poll_interval_sec: 1 # optional, default 1 (own default fn) # ...remaining keys identical to Postgres, including the optional `reaper`| Key | Type | Required? | Notes |
|---|---|---|---|
queue.kind | tag: Redis | Postgres | Sqlite | required | queue.rs:6-14 |
Redis (RedisQueueConfig, queue.rs:16-28) | |||
queue.uri | String | required | queue.rs:18 |
queue.dangerously_flush | bool | optional — #[serde(default)] | queue.rs:20 |
queue.queues | Option<Vec<String>> | optional | queue.rs:24. Declares named priority queues; first entry is most important |
queue.num_workers | u32 | optional, default 2 (num_workers()) | queue.rs:26-27 |
queue.reaper | Option<ReaperConfig> | optional, default None (disabled) | queue.rs:29-31. See below |
Postgres (PostgresQueueConfig, queue.rs:30-57) | |||
queue.uri | String | required | queue.rs:32 |
queue.dangerously_flush | bool | optional, default false | queue.rs:34-35 |
queue.enable_logging | bool | optional, default false | queue.rs:37-38 |
queue.max_connections | u32 | optional, default 20 (db_max_conn()) | queue.rs:40-41 |
queue.min_connections | u32 | optional, default 1 (db_min_conn()) | queue.rs:43-44 |
queue.connect_timeout | u64 (ms) | optional, default 500 (db_connect_timeout()) | queue.rs:46-47 |
queue.idle_timeout | u64 (ms) | optional, default 500 (db_idle_timeout()) | queue.rs:49-50 |
queue.poll_interval_sec | u32 | optional, default 1 (pgq_poll_interval()) | queue.rs:52-53 |
queue.num_workers | u32 | optional, default 2 | queue.rs:55-56 |
queue.reaper | Option<ReaperConfig> | optional, default None (disabled) | queue.rs:57-59. See below |
Sqlite (SqliteQueueConfig, queue.rs:59-86) | |||
| — | identical fields to Postgres, including queue.reaper | poll_interval_sec defaults via its own sqlt_poll_interval()=1 (queue.rs:81-82,92-94); all other defaults are shared with Postgres via the same db_* helper functions | |
ReaperConfig (queue.rs, all three backends) | Opt-in visibility-timeout reaper: when set, the queue provider spawns a background task that periodically requeues jobs stuck in processing (e.g. after a worker crash), reusing the same logic as cargo loco jobs requeue. Leaving it unset keeps the previous behavior — no automatic requeue. | ||
queue.reaper.age_minutes | i64 | required (only if reaper is set) | Requeue jobs that have been processing for longer than this many minutes |
queue.reaper.interval_seconds | u64 | optional, default 60 (default_reaper_interval_seconds()) | How often the reaper sweeps for stale jobs |
cache
enum CacheConfig — src/config/cache.rs:4-16, #[serde(tag = "kind")]. Default variant: Null (#[default], cache.rs:14-15) — this is what Config.cache’s #[serde(default)] produces when the cache key is omitted entirely.
cache: kind: InMem # requires the `cache_inmem` feature max_capacity: 33554432 # optional, default 33554432 bytes (32 MiB)
# --- or ---cache: kind: Redis # requires the `cache_redis` feature uri: redis://... # required max_size: 100 # required — max pool connections
# --- or (default) ---cache: kind: Null # no-op cache; used when `cache` key is omitted| Key | Type | Required? | Notes |
|---|---|---|---|
cache.kind | tag: InMem | Redis | Null | required if cache present | cache.rs:6-16 |
InMem (InMemCacheConfig, cache.rs:18-22) — feature-gated on cache_inmem | |||
cache.max_capacity | u64 | optional, default 33554432 (32 * 1024 * 1024, cache_in_mem_max_capacity()) | cache.rs:20-21,24-26 |
Redis (RedisCacheConfig, cache.rs:28-33) — feature-gated on cache_redis | |||
cache.uri | String | required | cache.rs:30 |
cache.max_size | u32 | required — max pool connections | cache.rs:32 |
| Null | (no fields) | — | default no-op cache |
If the corresponding feature (cache_inmem / cache_redis) is not compiled in, that kind value will fail to deserialize.
initializers, settings, scheduler
initializers: Option<BTreeMap<String, serde_json::Value>>(mod.rs:75,106) — a free-form map consumed by app initializers (e.g. anoauth2initializer readinginitializers.oauth2). Keys and shapes are defined by whichever initializer reads them, not byConfigitself.settings: Option<serde_json::Value>(mod.rs:88-89) — arbitrary app-defined settings, deserialize your own type fromctx.config.settings.scheduler: Option<scheduler::Config>(mod.rs:91) — struct and keys owned by the scheduler area; not detailed on this page.
Environment variables
| Variable | Purpose | Source |
|---|---|---|
LOCO_ENV | Selects the active environment; highest precedence | src/environment.rs:22,34 |
RAILS_ENV | Falls back to this if LOCO_ENV is unset | src/environment.rs:23,35 |
NODE_ENV | Falls back to this if both above are unset | src/environment.rs:24,36 |
LOCO_CONFIG_FOLDER | Overrides the config/ folder Loco loads from | src/env_vars.rs:16, read in Environment::load (src/environment.rs:59-64) |
LOCO_DATA | Data folder path | src/env_vars.rs:20 |
LOCO_POSTGRES_DB_OPTIONS | Extra Postgres connection options (only meaningful with with-db) | src/env_vars.rs:8 |
SCHEDULER_CONFIG | Path to the scheduler config file | src/env_vars.rs:18 |
RUST_BACKTRACE | Effectively forced to 1 when logger.pretty_backtrace: true | logger init |
any name passed to get_env(name=.., default=..) in a config YAML file | Injected into the rendered YAML by Tera’s built-in get_env function at load time | src/tera.rs:5-8 (Tera, not Loco code) |
Secrets (JWT secret, SMTP password, database uri credentials) are plain String fields with no dedicated vault type; the convention is to inject them via {{ get_env(name="...") }} at config-load time rather than hardcoding them in the YAML file.