Skip to content

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
EnvironmentWhen it’s usedFramework defaults
developmentLocal dev serverVerbose errors, hot reload, debug helpers visible
testingTest suite runsDebug UI off, /wheels/* public component disabled, caching on, URL environment switching blocked by default
productionReal usersErrors 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.

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 the this scope of Application.cfc (app name, session timeout). It’s included from the Application.cfc pseudo-constructor, so it executes on every request — its this-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.

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.

illustrative — config/settings.cfm
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.

The active environment is whatever config/environment.cfm sets. A generated app reads it from WHEELS_ENV:

illustrative — config/environment.cfm (as generated, comments trimmed)
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=development line the scaffold writes into .env applies.
  • On a server, set WHEELS_ENV=production in that server’s .env, or export it before the app starts (a Dockerfile ENV, a systemd unit or a deploy script). A WHEELS_ENV line in .env wins over an exported value, so a production server’s .env must not say development. Docker images built from wheels deploy init leave .env out (.dockerignore), so the Dockerfile’s ENV WHEELS_ENV=production applies.
  • 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.

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 reload CLI 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.

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:

illustrative — config/production/settings.cfm
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.