Core Concepts
Environments and Configuration
Wheels ships with three named environments, a small stack of configuration files that load in a fixed order, and one pattern — set(key=value) — for writing into the settings struct. Knowing which file overrides which, and where secrets belong, keeps local behavior from leaking into production and keeps production credentials out of your repository.
You’ll learn:
- The three environments and what each is tuned for
- Where configuration lives and the order files load in
- How
set()writes into the settings struct and how overrides cascade - How the active environment is chosen at boot
- How to switch environments at runtime — and what gates it
- Where secrets belong — and where they don’t
The three environments
Section titled “The three environments”| Environment | When it’s used | Framework defaults |
|---|---|---|
development | Local dev server | Verbose errors, hot reload, debug helpers visible |
testing | Test suite runs | Debug UI off, /wheels/* public component disabled, caching on, URL environment switching blocked by default |
production | Real users | Errors hidden, caching on, debug UI and /wheels/* public component disabled |
A fourth value, maintenance, is supported for planned downtime. Most apps never need it.
These are the defaults the framework itself flips per environment. Operational concerns like log rotation and HTTPS enforcement are not framework settings — handle those in your web server, or use the SecurityHeaders middleware for transport-security headers.
In development, the framework error page (showErrorInformation=true) includes a Copy button that puts a JSON dump of the exception — type, message, suggested action, file and line, a source snippet, and stack frames tagged as app vs framework — on the clipboard so you can paste it into a coding agent.
Where settings live
Section titled “Where settings live”Configuration loads from a small set of files in config/. The framework reads them in order at application start and on every reload (?reload=true&password=...):
config/app.cfm— hooks for thethisscope ofApplication.cfc(app name, session timeout). It’s included from theApplication.cfcpseudo-constructor, so it executes on every request — itsthis-scope effects apply at application start, but don’t put side effects in it that aren’t safe to run per request.config/environment.cfm— sets the active environment name. This runs first at application start: the framework derives its environment-dependent defaults (error verbosity, caching, debug UI) from this value before any other config file loads.config/settings.cfm— shared defaults that apply to every environment. Middleware list, datasource name, reload password.config/<environment>/settings.cfm— per-environment overrides.config/development/settings.cfm,config/testing/settings.cfm,config/production/settings.cfm. Loaded after the shared file, so these win.config/services.cfm— DI container registrations. See The Dependency Injection Container.config/routes.cfm— route definitions. See How Routing Works.
The per-environment override directory is only read for the environment you’re in. config/production/settings.cfm never runs in development, which is exactly what you want when production is where the extra caching belongs.
The set() pattern
Section titled “The set() pattern”Every framework setting is written with one function: set(key=value). It writes into the Wheels settings struct the framework reads from during boot and dispatch. There is no YAML, no .ini, no configuration DSL — just CFML calling a setter.
set(dataSourceName = "myapp_dev");set(reloadPassword = env("WHEELS_RELOAD_PASSWORD", ""));One exception: don’t set(environment=...) here. Environment selection belongs in config/environment.cfm only — by the time settings.cfm runs, the framework has already derived its environment-dependent defaults, so setting the environment this late produces a half-switched app (the environment reports production while verbose error details, development caching flags, and allowMigrationDown persist from the boot-time value).
Also note that a blank reloadPassword disables URL-based reload entirely: since 4.0.4 the gate fails closed, so ?reload=true requires a non-empty configured password and a matching password parameter (#3062). Set a real reload password in every environment where you want reload over HTTP to work.
Settings cascade: framework defaults load first, then config/settings.cfm, then config/<environment>/settings.cfm. Later writes win. A value you set in the shared file is the default for every environment; the environment-specific file only needs to name the keys it changes.
One development-only default worth noting: reloadOnGlobalChange (defaults true in development) makes bare ?reload=true re-include app/global/*.cfm when any tracked file’s mtime changes, so new helpers added to app/global/functions.cfm are picked up immediately without the password-gated full reload. Opt out with set(reloadOnGlobalChange=false) in config/settings.cfm. The setting defaults to false in all other environments, so there is no DirectoryList overhead in production.
Another: strictArguments (defaults "warn" in development, "off" elsewhere) logs an argument that an association, validation, finder or migration column doesn’t know, such as foreign_key or orderby, instead of ignoring it. set(strictArguments="throw") makes it an error. See Strict Arguments.
Environment detection
Section titled “Environment detection”The active environment is whatever config/environment.cfm sets. A generated app reads it from WHEELS_ENV:
local.wheelsEnvironment = Trim(env("WHEELS_ENV", ""));if (!Len(local.wheelsEnvironment)) { local.wheelsEnvironment = "development";}if (!ListFindNoCase("development,testing,maintenance,production", local.wheelsEnvironment)) { Throw(type = "Wheels.InvalidEnvironment", message = "WHEELS_ENV must be development, testing, maintenance or production.");}set(environment = LCase(local.wheelsEnvironment));An unset or empty WHEELS_ENV means development, so a new app runs locally with no setup. Any value other than the four environment names stops the app from starting (a “Wheels failed to initialize” page, with the value in the server log), because Wheels treats an unknown environment like development where error output is concerned: a typo such as prod must not quietly expose error details.
env() checks .env first, then the process environment, so:
- In development, the
WHEELS_ENV=developmentline the scaffold writes into.envapplies. - On a server, set
WHEELS_ENV=productionin that server’s.env, or export it before the app starts (a DockerfileENV, a systemd unit or a deploy script). AWHEELS_ENVline in.envwins over an exported value, so a production server’s.envmust not saydevelopment. Docker images built fromwheels deploy initleave.envout (.dockerignore), so the Dockerfile’sENV WHEELS_ENV=productionapplies. - With neither, the app runs in
development. Production deploys always set the variable explicitly.
The wheels CLI doesn’t read the variable, and WHEELS_ENV also selects which .env.<name> overlay file loads at boot.
Switching environments at runtime
Section titled “Switching environments at runtime”A running app can switch environments without a redeploy: request ?reload=<environment>&password=<reloadPassword> (for example ?reload=testing&password=...). The framework restarts the application, carries the switch parameters through the restart redirect, and boots into the requested environment — including switches into production or maintenance, after which the redirect strips the parameters. Requesting the environment you’re already in is a no-op; for a plain same-environment restart use ?reload=true&password=<reloadPassword>, which strips the reload parameters from the redirect.
The gate is allowEnvironmentSwitchViaUrl. Any explicit boolean you set() is honored in every environment. If you never touch it, the default is false in production, testing, and maintenance, and true everywhere else. Leave it disabled in production.
In development, the debug bar’s Environment panel offers the same switch as quick-switch links: they render when a reloadPassword is configured and switching via the URL is allowed, and clicking one prompts for the reload password (it is never embedded in the page).
One caveat, now fixed:
- Before 4.0.4, the
wheels reloadCLI command could report success even when the reload didn’t take effect (#3059). It now checks for the framework’s restart redirect (302), reports anything else in red, and exits non-zero.
Secrets handling
Section titled “Secrets handling”Don’t commit .env, credentials, or API keys. The scaffold ships a .gitignore that excludes .env for exactly this reason. Read secrets at boot with env("VAR_NAME") — the canonical accessor, which checks your .env first, then OS environment variables — optionally passing a default for missing-but-not-fatal values:
set(dataSourcePassword = env("DB_PASSWORD"));set(stripeApiKey = env("STRIPE_API_KEY", ""));Local development uses .env files; production uses a secrets manager (1Password Connect, AWS Secrets Manager, HashiCorp Vault) that injects the same variable names at deploy time. Your configuration code doesn’t change between environments — only the source of the values does.
See also
Section titled “See also”- The Dependency Injection Container — also loaded at boot
- Installing Wheels — environment-variable setup
- Deployment Overview — environment per deploy target