Part 5 of "From 2.x to 4.x: the field guide". CFWheels 2.x ran validation condition= and unless= strings through Evaluate(); Wheels 4.1 parses a small grammar and throws Wheels.InvalidValidationCondition for what it can't parse. This post lists the grammar 4.1.2 accepts, runs ten 2.x-style strings on 4.1.2 (three throw, seven give the wrong answer without an error), rewrites each one, covers the this.method() escape hatch and the two production conditions from the 4.1.1 CHANGELOG, and explains what Wheels 4.2 changes.
Part 4 of "From 2.x to 4.x: the field guide" covers public/Application.cfc, the app-owned file that a vendor/wheels/ swap never updates. It replays every template change from 4.0.0 to 4.1.2 one commit at a time: the DI container, the onError fallbacks, the fail-closed reload gate, the X-Wheels-Reload-Password header, IP-based debug access, the session cookie, the Adobe teardown handlers, the jBCrypt load path and the .env parser. A table says what each edit fixes, which release added it and whether skipping it fails safe, and the post shows what changed on Lucee 7 and Adobe ColdFusion 2025 after each edit.
Part 3 of "From 2.x to 4.1: the field guide" takes the example app from Wheels 3.0.0+33 to 4.1.2. It reads the real `wheels upgrade check` output before and after, explains why to run the check before swapping vendor/wheels, and walks the 14 commits: the framework swap, the 4.1 public/Application.cfc, plugin replacements, paginationNav(), the CSRF settings, a RocketUnit-to-WheelsTest conversion and validation conditions. It also covers the breakers only a real boot found: default='' in migrations, structs assigned to scalar columns, dates interpolated into where strings on 4.1.2, and a reload password the CLI and the app disagreed on.
Part 2 of "From 2.x to 4.1: the field guide" walks the nine commits that take the example app from CFWheels 2.2 to Wheels 3.0.0+33: moving code into app/ and assets into public/, the new public/Application.cfc, vendor/wheels pinned to 3.0.0+33, path and test fixes, and the problems the 3.0 guide doesn't mention: events/ has to move, plugins/ stays at the root, a caret range can install a 4.0 development build, joinType="left" silently drops a nested join, and a 2.3 validatesConfirmationOf change breaks login.
The first post in "From 2.x to 4.1: the field guide". Going from Wheels 2.x to 4.1 takes two passes: 2.x to 3.0.0+33 (the layout move), then 3.0 to 4.1 (the framework swap and the 4.x breaking changes). This post shows what each pass touches, which files no upgrade command ever touches, and the real CFWheels 2.2 app we upgrade on camera, wheels-dev/cfwheels-example-app, with one branch and one commit per step.
Wheels 4.1.2 is a patch release on 4.1.x: `wheels deploy` now builds and pushes the image it deploys, failing CLI commands exit non-zero, migrations can opt out of the per-step transaction, a new `baseUrl` setting, migrator and model fixes, tighter gates on the development tools, and four security fixes. Upgrading is recommended; a few generated-app files need small edits.
Wheels 4.1.1 fixes a CLI security issue (GHSA-x3cm-2j3q-jgg4), makes the first request after a restart work on Adobe ColdFusion, stops multi-database migrations from stalling, and clears out the rough edges people hit in 4.1.0. Every compatibility-matrix leg passed, test by test, before it shipped.
Wheels 4.1.0 is out: bcrypt, one-line session auth, pretty routes, DI factories, CLI diff/dry-run/offline, a security hardening pass, and model instantiation ~2.5x faster. Every one of those threads traces back to a single issue filed by @chapmandu — and the release is better because it didn't start with a features list.
There was a controller with nine hundred lines and one bug that only fired in September. Three developers touched it that week. When we pointed the complexity panel at it, the score said everything — and the coverage run said the test suite had never touched it. 4.1 ships the two tools that make that obvious before the September bug goes out.
A security review of Wheels 4.0 found a pile of "surely that's safe" that weren't: a policy that treated the string "yes" as true, a form method override that worked on a GET, a zip that could escape its directory. 4.1 closes them by failing closed. Here's the story area by area, and what you have to check on the way up.
@chapmandu reported his ported app's test suite running nearly six times slower on Wheels 4 than on 2.5. We didn't believe it, so we built a benchmark rig to prove him wrong. He was right. Here's the three-month investigation, two unrelated bugs it surfaced, and the lesson about trusting micro-benchmarks.
A deploy pipeline went green while doing nothing to the database, and a code review approved a generator run that had already written files. Both were "the CLI guessed wrong." 4.1 adds diff, dry-run, and offline mode — and the honesty fixes that make a green build mean something.
A storage service had to pick S3 in production and the local disk in dev. The to() binding couldn't express that, so the decision leaked into every caller. toFactory() gives a DI container a *how* — and it's the difference between one closed arrow and ten open switch statements.
A support engineer pastes a link into a customer chat, and they get /posts/4821. Nobody shares /posts/4821. bindBy="slug" exists to make resource URLs say something — and it comes with one mistake you can make that quietly breaks your site.
The enableSession() facade is one line of config. But the real story is the three subtle bugs it exists to prevent — the ones that only surface after the second deploy, or only on a reload, or only for a user who was already logged in. Here's what happens when the wiring goes right.
The story of how bcryptHash(), bcryptVerify(), and bcryptNeedsRehash() made it into Wheels 4.1 starts with a pentest finding and a migration that took three weeks. Here's the helpers, the cost trade-off, and the part where Adobe CF made us question our sanity.
Wheels 4.1 isn't another "we added features and fixed bugs" release. It's the release where a single bug report sent us back to the drawing board, where a security pass made the things you always assumed were safe actually be safe, and where the framework got meaningfully faster. Here's what's coming, and the story behind it.
Wheels 4.0.6 is the first 4.0 feature release after 4.0.5's hardening pass — one-command authentication (`wheels generate auth`), an authorization policy layer, pluggable local/S3 storage disks, query-builder chains that can start with `select()`, and the mixin-plan cache that was making 4.0.x feel slow. Also: Adobe `onApplicationEnd` teardown, tenant/pagination request-key collisions, test-runner isolation, and a production gate on `/wheels`.
i18n usually arrives as an emergency. wheels-i18n makes the mechanical part boring: t() and tp() with interpolation and pluralization, JSON or database catalogs, session-based locale switching, and the fallback chain that makes incremental translation actually shippable.
Most developers use a fifth of the debug bar and writeDump() around the rest. The full tour: what each panel answers, the developer tools behind the last tab, the throughput cost worth knowing, and why the bar can't follow you to production — by design.
Declare fullName or commentCount once as SQL on the model and it behaves like a column everywhere — sortable, selectable, never stale. And Wheels 4's select=false + includeCalculated pair fixes the cost problem calculated properties always created: expensive expressions run only where they're asked for, additively.
A REPL inside your running app: models resolve, scopes chain, the DI container answers, settings read back. The sessions that replace writeDump-and-refresh archaeology, the slash-commands, and why console and repl are different tools that look alike.
SSE gives you the pipe; channels answer the harder question — how the model callback that knows something happened reaches the connection three requests away. publish(), subscribeToChannel(), channelSSETag(), the memory-vs-database adapter decision, and the named-event gotcha that catches almost everyone.
findAll on 400,000 rows materializes everything into memory at once — and the request cache holds it there. Wheels 4's findEach and findInBatches walk big result sets a page at a time, with three built-in optimizations you'd forget to hand-roll and two sharp edges worth knowing first.
CommandBox teams do not have to switch. The complete box-native workflow for Wheels 4 — install from ForgeBox, engine selection with cfengine, datasources via CFConfig, every framework task's in-app equivalent, CI with setup-commandbox, and an honest accounting of what the wheels CLI adds if you ever want it.
Why exporting WHEELS_ENV=production doesn't flip your app into production — and everything else about the config pipeline: the three-file load order, the .env chain that shadows OS variables, what production mode really changes, and the reload gate's fail-closed mechanics.
Three small pieces of plumbing turn 'the site gave me an error around lunchtime' into a single grep: the RequestId middleware, the /up health endpoint your app already ships, and a forty-line JSON logging pattern. Plus the two health-endpoint mistakes everyone makes once.
Wheels 4 denies cross-origin requests until you say otherwise — and the Cors middleware refuses the popular ways of saying it wrong. The deny-all breaking change, single-origin resolution, Vary: Origin, preflight handling, and the legacy-settings trap the framework defuses for you.
Wheels 4 ships session, API-token, and JWT authentication as pluggable strategies behind one Authenticator registry. Wiring up all three doors — with the sharp edges: private filters, dynamic finders, the 32-byte JWT secret minimum, and the inline-closure constructor that crashes Adobe ColdFusion.
The manual install that took the community two days to reverse-engineer in Wheels 3 is now two zip files and zero external dependencies. How to install, migrate, test, seed, and upgrade a Wheels 4 app with no CLI, no CommandBox, and no package manager — plus the new guides that document every step.
A complete how-to for sending email in Wheels 4.0 — the sendEmail controller helper, how email view templates resolve, multipart text+html, attachments, and the correct way to deliver mail from a background job. Includes the sharp edges that actually bite.
Wheels 4.0 has no ActiveStorage-style attachment system — and that's fine. This how-to walks the honest idiomatic upload flow end to end: fileField/fileFieldTag form helpers, the startFormTag multipart trap, native cffile in the controller, model validation over the result, and traversal-safe downloads with sendFile.
A practical, source-grounded tour of every cache Wheels 4.0 actually ships — action caching, query caching, partial and page caching — plus the honest truth about invalidation: there's no clearCache(key), so you live on short expiry times and full reloads.
A comprehensive Wheels 4.0 guide to model associations — hasMany, belongsTo, and hasOne, the exact dynamic methods each one generates, dependent-delete cascades, nested properties from form params, eager loading with include=, has-many-through, and polymorphic associations — with the sharp edges that bite in practice.
A worked how-to on Wheels 4.0 form helpers: object-bound fields that round-trip into params.user.email, the real HTML5 single-input helpers, and the two error-rendering helpers — plus the sharp edges that bite first.
A practical Wheels 4.0 guide to the full validation surface — conditional rules with condition/unless/when, custom validate() methods, enum() integration, and how to read the error API correctly. Built end-to-end around one User model, with every behavior grounded in the framework source.
A complete Wheels 4.0 routing guide — RESTful resources, nested resources via callback vs manual nested=true, and route model binding that resolves params.post before your action runs. Plus the sharp edges: route order, the protected-helper-names trap, and how a missing record 404s.
Wheels 4.0 ships a convention-based seeder built around seedOnce() — an idempotent helper that creates a record only if a matching one doesn't already exist, wrapped in a transaction that rolls the whole run back on any failure. This is the worked guide: file conventions, the CLI, composite uniqueness, and the sharp edges.
A user-facing how-to for the Wheels 4.0 DI container: register services in config/services.cfm with singleton/request/transient scopes, resolve them anywhere with service(), declare them in controllers with inject(), and let constructor auto-wiring fill in the rest.
A from-scratch guide to authoring custom middleware in Wheels 4.0 — the one-method interface, the singleton-per-app lifecycle and the thread-safety it demands, how to short-circuit, and how to register globally or per route.
A complete how-to on Wheels 4.0's paginationNav helper — the all-in-one nav, the bootstrap5/bootstrap4/tailwind preset system, the auto/always/never anchor tri-state, and the dev-time guardrails that catch typos before they ship.
A worked guide to Wheels 4.0's Server-Sent Events helpers — renderSSE for one-shot events, the initSSEStream/sendSSEEvent/closeSSEStream trio for long-lived streams, plus isSSERequest content negotiation, keep-alive heartbeats, and the engine-fragility sharp edges to test for.
Wheels 4.0 ships three model-side features that compose into one design: scopes name reusable query fragments, enums turn property values into auto-generated checkers and scopes, and the chainable query builder swaps raw WHERE strings for fluent, type-aware composition. This post walks all three through one worked example and surfaces the bug I hit while writing it.
Wheels 4.0.5 is out, and together with 4.0.4 it's one of the most substantial hardening passes on the 4.0 line — 100+ changes spanning security (open-redirect and info-disclosure fixes, fail-closed gates, SQL-surface tightening), warm-path performance, a much tougher `wheels deploy`, and Adobe CF / BoxLang cross-engine fixes. 4.0.5 then makes the whole thing installable the same way on every major platform — Homebrew, Scoop, apt, dnf — including arm64 Linux, verified daily.
Wheels 4.0 ships a stdio MCP server that exposes the CLI to AI editors — not as a chat sidekick, but as a tool surface a model can call. This post walks the architecture (reflection over a single CFC), builds a commenting feature end-to-end via Claude, and is honest about the config-template drift I hit while writing it.
A broad how-to on the Wheels 4.0 migrator: write up()/down() migrations, drive the TableDefinition column builder (including the three-column timestamps() and the columnNames-preferred helpers), seed with inline SQL, and run the migrate CLI — including the doctor/info/forget/pretend commands that keep a shared dev database honest.
Wheels 4.0 replaces plugins with a package system that treats your filesystem as the registry. This post takes a small package from empty directory to installed-and-mixed-in, names every field in the manifest, and is honest about the rough edges I hit while writing it.
Wheels 4.0.3 is the third patch on the 4.0 line, focused on making the `wheels` CLI trustworthy in scripts and CI: argument parsing is rebuilt end-to-end (`--no-*` negations and named-only flags finally reach every command), failures exit non-zero, and write-side commands refuse to attach to a different project's server. Plus PostgreSQL/CockroachDB foreign-key migration fixes, pre-23c Oracle support, preserved column casing in model output, and a fix that stops framework helpers from being URL-invokable as controller actions.
Wheels 4.0.2 is the second patch on the 4.0 line. It teaches the migrator how to cope with a database that more than one developer shares — orphan-version detection, a `migrate doctor` health report, and `forget` / `pretend` reconciliation commands — fixes a class of silent migration rollbacks on MSSQL, makes the migrator's column-name helpers consistent, and ships native signed apt.wheels.dev / yum.wheels.dev package repositories so Linux installs and upgrades are a one-liner.
Wheels 4.0.1 is out — the first patch on the 4.0 line. It hardens Adobe ColdFusion 2023/2025 compatibility, fixes the Windows Scoop install regressions reported after GA, adds CSS-framework presets to paginationNav(), short-circuits whereIn([]) so empty filters stop emitting invalid SQL, and threads about a hundred smaller fixes through the rest of the framework.
Wheels 4.0 ships a real middleware pipeline, and three of the things you used to reach for a plugin to add — rate limiting, CORS, and security headers — are built in. This post walks through wiring a JSON API with per-API-key rate limits, sane proxy handling, and the right ordering so you do not accidentally weaken the very headers you were trying to add.
Wheels 4.0 decomposes the framework's rim against the CFML engine, pulls dependency injection and the test runner in-house as wheelsdi and WheelsTest, and breaks the monolithic boot sequence into discrete phases. Nothing a user of the framework notices at the surface. Everything a contributor notices when they try to add the next feature.
Wheels 4.0 lands with seven breaking changes and a Legacy Compatibility Adapter for teams that cannot touch every call site this quarter. This post is the honest map: what breaks, how to detect it, how to fix it, and when the adapter is the right answer instead.
For years, the same rows showed up red in every framework comparison: no bulk ops, no polymorphic associations, no advisory locks, no middleware, no browser testing. Wheels 4.0 closes those gaps. This is a guided tour of what changed and where the framework still trails.
Wheels 4.0 ships with a new inner-loop story: a single native binary, a sixty-second full test run, and a multi-phase migration that quietly moved the framework off Docker for day-to-day development. This post walks through how we got here, what LuCLI is, and why cross-engine matrix testing still belongs in Docker.
Wheels 4.0 lifts multi-tenancy out of plugin territory and into the framework core. Per-request datasource switching catches every query and every job on the way to the database, and tenant-aware background jobs come along for the ride.
Wheels 4.0 ships a production-ready job queue that needs only your database. You get a persistent CLI worker, live monitoring, configurable backoff, and tenant-aware enqueueing without any extra services. Redis still wins for very high throughput and pub/sub fan-out — this post names where the DB-backed queue is the right call and where it is not.
Wheels 4.0 completes the test pyramid: BDD unit tests, a fluent HTTP TestClient for integration, and Playwright-powered browser automation for end-to-end — all running in parallel. The category that was most embarrassing in 3.0 is the most complete in 4.0.
Wheels 4.0 shipped more than forty security-hardening pull requests across eight categories — SQL, path handling, session integrity, CORS, rate limiting, auth and dev surfaces, CLI and MCP, and view helpers. The common thread is a shift in posture: the framework's defaults are now safe first, convenient second.
Wheels 4.0 ships with a new command: wheels deploy. It's a port of Basecamp's Kamal into the Wheels CLI — zero-downtime Dockerized deploys to Linux servers over plain SSH, no Ruby runtime required. This post covers what got ported, the one deliberate divergence from Kamal, and the byte-compatibility contract that lets you take over a Kamal-managed server without cleanup.