Purpose: Canonical inventory of every user-visible change merged into develop between the 3.0.0 stable release and today. Source of truth for blog posts, release notes, and the 3.0 → 4.0 comparison narrative.
Baseline: v3.0.0+33 — Wheels 3.0.0 stable release, tagged 2026-01-10 (CHANGELOG entry).
Audit range: 2026-01-12 → 2026-04-22 (approx. 15 weeks).
Initial audit date: 2026-04-16.
Refreshed: 2026-04-22 — added delta section below ("Post-2026-04-16 additions") covering 69 PRs merged in the subsequent 6-day window.
- Extracted all PRs merged to
developin the window viagh pr list --base develop --state merged --search "merged:>=2026-01-10" --limit 250. - Cross-referenced against
git log --merges v3.0.0+33..origin/develop. - Compared against the
[Unreleased]section of CHANGELOG.md. - Bucketed each PR by subsystem; dedupe'd multi-PR features into single entries with all PR links.
- Flagged CHANGELOG gaps — the
[Unreleased]section captured ~10 items; ~60 additional user-visible changes were not recorded.
- Total merged PRs: 260+ (185 through 2026-04-16 + 69 in the refresh window)
- Distinct user-visible features / changes: ~75 (after grouping multi-PR features; see delta section for the ~6 additions since 2026-04-16)
- Security-hardening PRs: 40+ (see Security Hardening section; unchanged in delta window)
- Breaking changes: 7 (see Breaking Changes section; unchanged in delta window — HTTP MCP deprecation in #2140 emits a warning but does not remove the endpoint)
- Contributors: @bpamiri (Peter Amiri), @zainforbjs, @chapmandu, @mlibbe, @MukundaKatta, plus Dependabot
- CHANGELOG coverage gap:
[Unreleased]missed ~60 user-visible items — blog + CHANGELOG catch-up work recommended.
New capabilities:
- Chainable query builder (#1922) —
where(),orWhere(),whereNull(),whereBetween(),whereIn(),orderBy(),limit(),get(). Injection-safe fluent queries as an alternative to raw WHERE strings. Composes with scopes. - Enum support (#1921) —
enum(property="status", values="draft,published,archived")auto-generatesisDraft()/isPublished()checkers,draft()/published()scopes, and inclusion validation. Supports ordered lists and value maps. - Query scopes (#1920) —
scope(name="active", where="...")and dynamic scope handlers. Composable and chainable:model("User").active().recent().findAll(). - Batch processing (#1919) —
findEach(batchSize, callback)andfindInBatches()for memory-efficient iteration. Works with scopes. - Bulk insert / upsert (#2101) —
insertAll(records)andupsertAll(records, uniqueBy)with per-adapter native UPSERT syntax (MySQL, PostgreSQL, SQL Server, SQLite, H2, CockroachDB, Oracle). - Polymorphic associations (#2104) —
belongsTo(polymorphic=true)andhasMany(as=)with type-discriminator JOINs. - Advisory locks + pessimistic locking (#2103) —
withAdvisoryLock(name, callback)with try/finally release;.forUpdate()on QueryBuilder forSELECT ... FOR UPDATE. - CockroachDB adapter (#1876, #1986, #1993, #1999) — seventh supported database. Full SQL generation,
RETURNINGclause identity select,unique_rowid()PK convention, test matrix inclusion. throwOnColumnNotFoundsetting (#1938) — opt-in strictness for unknown columns in WHERE clauses (helps catch typos at dev time).
Hardening / fixes:
- SQL identifier quoting for reserved words (#1874) — prevents reserved-word conflicts in table/column names.
- Calculated-property SQL validated at config time (#2067) — catches broken SQL before request time.
- GROUP BY validation with dot-notation (#2084) — parity with ORDER BY parser.
- Auto-migrations from models (#2102) —
AutoMigrator.diff(modelName)compares model property definitions against the current DB schema and returns add/remove/change column lists.generateMigrationCFC()produces a migration CFC withup()/down(). - Auto-migration rename detection (#2112) — explicit hints (
renames={"old": "new"}) plus heuristic suggestions (normalized-token + Levenshtein, configurable threshold). Newwheels dbmigrate diffCLI command and MCPwheels_migrate(action="diff"). - Gap migration detection (#1928) —
migrateTo()now detects and runs any previously-skipped migrations in the target range, not just the endpoint.
- Router modernization (#1891 / #1894) —
group()helper for route grouping, typed constraints (whereNumber,whereAlpha,whereUuid,whereSlug,whereIn), API versioning via.version(1), performance indexes for faster route lookup. - Route model binding (#1929) —
binding=trueon resource routes orset(routeModelBinding=true)globally. Auto-resolvesparams.keyintoparams.<modelName>before the controller action runs. ThrowsWheels.RecordNotFound(404) on miss.
- View lookup fix after renderText/renderWith (#1991) — fixes #1961; prevents
renderWithfrom breaking subsequent view lookups. - CSRF key enforced in production + JWT algorithm validation (#2079) — fails fast in production when CSRF key is empty; validates JWT
algclaim to prevent algorithm-confusion attacks. - Path traversal check hardened against encoded bypass (#2089).
- Composable pagination view helpers (#1930) —
paginationNav(),paginationInfo(),firstPageLink(),previousPageLink(),pageNumberLinks(),nextPageLink(),lastPageLink(). Replaces monolithicpaginationLinks()as the idiomatic pattern for building custom pagination UIs. Old helper retained for back-compat. - Architecture hardening: XSS helpers, error hooks, interface verification (#2097) — adds
h(),hAttr(),stripTags(),stripLinks()formally; interface verification ensures helper contracts; error-rendering hooks. - Redesigned congratulations page (#2098) — new landing page for
wheels newscaffolded apps. - Path traversal validation in partial rendering (#2071) — blocks
includePartial("../../secrets.cfm")-style attacks. - XSS pagination hardening (#2042, #2057, #2060) — sanitize
prependToPage,anchorDivider,appendToPage; prevent HTML-entity-encoding bypasses.
- Middleware pipeline (#1924) — core framework: closure-based middleware chain, runs at dispatch level before controller instantiation, route-scoped via
.scope(middleware=[...]), global viaset(middleware=[...]). ImplementsMiddlewareInterface(handle(request, next)). - Rate limiting middleware (#1931) —
wheels.middleware.RateLimiterwith three strategies:fixedWindow,slidingWindow,tokenBucket. Storage backends: in-memory (default) and database-backed (auto-createswheels_rate_limitstable). EmitsX-RateLimit-*headers andRetry-Afteron 429. - CORS middleware (built into middleware pipeline) — hardened defaults (deny-all instead of wildcard, #2039), rejects wildcard origin with credentials (#2053).
- Security headers middleware — CSP, HSTS, Permissions-Policy (#2036); HSTS default-on in production (#2081).
- CSRF cookie hardening (#2027, #2035, #2054, #2079) — SameSite attribute, auto-generated encryption key when empty, key required in production.
- Session fixation prevention (#2034) — regenerate session on login.
- Open-redirect prevention in
redirectTo()(#2038).
Rate-limiter hardening follow-ups: #2024 (trustProxy default false), #2041 (memory exhaustion + IP spoofing), #2048 (per-key exhaustion), #2069 (fail-closed on lock timeout), #2080 (cleanup throttle, key length limit), #2088 (proxy strategy default = last).
- Job worker daemon (#1934) —
wheels jobs work/status/retry/purge/monitorCLI commands. Persistent background job processing with optimistic locking, timeout recovery, live dashboard. Configurable exponential backoff viathis.baseDelayandthis.maxDelayin jobconfig(). Auto-createswheels_jobstable.
- Pub/sub channels for SSE (#1940) — channel subscription model on top of SSE: publish to channel → all subscribers receive events. Database-backed event persistence with
wheels_eventstable.subscribeToChannel(),publish(),poll(), cleanup of old events. Dual implementation (DatabaseAdapter + in-memory). - SSE newline injection hardening (#2051) — sanitizes event field and data values to prevent injection into the SSE stream.
- Multi-tenant support (#1951) — per-request datasource switching. Built-in (no external package). Supports tenant-aware background jobs natively.
- Expanded DI container (#1933) —
asRequestScoped()for per-request service instances, globalservice()helper, declarativeinject()in controllerconfig(),bind()interface binding, auto-wiring ofinit()arguments from registered names,config/services.cfmfor service registration at app startup. Scope support: transient, singleton, request-scoped.
- PackageLoader +
packages/→vendor/model (#1995) — optional first-party modules ship inpackages/, activated by copying tovendor/. Auto-discovered on startup viaPackageLoader.cfc. Each package has apackage.jsonwithprovides.mixinstargets (controller,view,model,global,none). Per-package error isolation (a broken package is logged and skipped; app continues). - Module system with dependency graph (#2017) — dependency resolution via topological sort (
requires/replaces/suggests), lazy loading opt-in per package. - Plugin component paths logged on load (#2085) — improves debugging.
- Deprecation path for legacy
plugins/folder (still works, warns on load).
- HTTP test client (
TestClient) (#2099) — fluent integration-testing DSL:TestClient.visit("/users").assertOk().assertSee("John"). Assertions for status codes, body content (assertSee/assertDontSee/assertSeeInOrder), JSON responses (assertJson/assertJsonPathwith dot notation), redirects, headers, cookies (tracked across requests for session support). - Parallel test runner (
ParallelRunner) (#2100) — discovers test bundles, partitions them across N workers via round-robin, fires parallel HTTP requests throughcfthread, aggregates JSON results. Configurable worker count and timeout. - Browser testing via Playwright Java (#2113, #2115, #2116, #2121, #2122) —
BrowserTestbase class with fluent DSL wrapping Playwright Java. Methods: navigation (visit,visitRoute,back,forward), interaction (click,fill,type,select,check,attach,dragAndDrop), keyboard (keys,pressEnter,pressTab,pressEscape), waiting (waitFor,waitForText,waitForUrl), scoping (within), cookies (setCookie,cookie,clearCookies), auth (loginAs,logout), dialogs (acceptDialog,dismissDialog,dialogMessage), viewport (resize,resizeToMobile,resizeToTablet,resizeToDesktop), script (script,pause), screenshots, full assertion suite.wheels browser setupdownloads JARs + Chromium (~370MB). CI workflow runs browser specs across pr.yml and snapshot.yml. testbox→wheelstestnamespace rename (#1889) —extends="wheels.WheelsTest"replacesextends="wheels.Test"(legacy still works during 4.0).tests/specs/functions/→tests/specs/functional/rename (#1872).- Legacy RocketUnit removal (#1925) — WheelsTest (BDD syntax) is the only supported style for new tests in 4.0. Existing RocketUnit specs continue to run; no new ones.
- RocketUnit wildcard filter (#1857) — improves test selection for the remaining legacy specs.
CLI UX & generators:
wheels snippets→wheels generate snippetscommand rename (#1852) — breaking; aligns with the "code snippets" concept.- Scaffold, seed, in-process services (#2065) — Phase 3-4 of LuCLI migration. In-process service invocation removes external process overhead for generators.
- Playwright CLI commands (#2013, #2021) — configuration, test helpers.
- Oracle admin privilege check for DB creation (#1843).
- Docker command status messages (#1844, #1897, #2095).
LuCLI (strategic direction — zero-Docker, faster inner loop):
- Phase 2: local testing without Docker (#2063) —
tools/test-local.shruns the test suite on LuCLI + SQLite. ~60s for full core suite. - Phase 2 service layer, generators, MCP annotations (#1941).
- LuCLI-native CI pipeline (#2032) — Lucee 7 + SQLite in CI, matching local inner loop.
- LuCLI module distribution via wheels-cli-lucli repo (#2018).
- Tier 1 commands ported to LuCLI (#2092) + WheelsTest test suite for the module (#2093).
CLI security hardening:
- Shell argument sanitization in deploy commands (#2068, #2073).
- Command injection prevention in db shell (#2040).
- MCP hardening: path traversal (#2049, #2062), auth gate + input validation (#2050), error suppression (#2072), port validation (#2075), structural allowlist (#2083), CSRNG session tokens (#2087).
/wheels/mcpendpoint — Wheels tools exposed to AI coding assistants. Pre-existing but substantially hardened in 4.0 (see CLI security hardening section).- Documentation-reader path traversal hardening (#2049).
- Auth gate and input validation (#2050).
- Engine adapter modules (#2016) — W-004 project. Lucee, Adobe CF, BoxLang each get a dedicated adapter module encapsulating engine-specific behavior (struct member function idioms, scope handling, closure semantics).
- Engine adapter startup + cross-engine compatibility fixes (#2028, #2030, #2031).
- Railo compatibility workaround removed (#1987) — Railo is no longer a target; cleanup.
- Adobe Oracle coercion — removed (#2030) then restored (#2031) — net: preserved.
- Malformed percent-encoding crash fix (#2006) —
$canonicalizecatchesIllegalArgumentExceptioninstead of propagating.
- Interface contracts (#2014) — W-005 project. Formal contracts for key framework extension points. Middleware, strategies, adapters all have verifiable interfaces.
- Legacy compatibility adapter for 3.x → 4.0 migration (#2015) — W-003 project. Soft-landing for apps upgrading from 3.x.
env()helper (#1985) — cross-scope environment variable access.- Pre-request logging (#1895).
- Debug panel redesign (#2000, #2001) — W-001, W-002. Modernized dev-panel UI.
- Congratulations page redesign (#2098) — W landing.
allowEnvironmentSwitchViaUrldefaults to false in production (#2076).- Non-empty reload password required for env switching (#2082).
Beyond the middleware and controller items above, 40+ PRs hardened security across SQL generation, path handling, CORS/CSRF, console endpoints, and MCP:
SQL injection:
- QueryBuilder property + operator validation (#2025).
- ORDER BY clause (#2026).
$quoteValuesingle-quote escaping (#2033).- Scope handler argument sanitization (#2043, #2045, #2056, #2061, #2070, #2090).
- Geography property detection (#2044).
- WKT handling (#2055).
- Enum scope WHERE clauses (#2023, #2056, #2070).
includeparam in UPDATE queries (#2047).- Index hints via
$indexHint(#2058).
Path traversal:
- Partial template rendering (#2071).
guideImageendpoint (#2037).- MCP documentation reader (#2049).
- Encoded-bypass attempts (#2089).
Console / reload:
consoleevalhardened: POST-only, robust IPv6, Content-Type checks (#2059), constant-time comparison and rate-limiting on reload (#2077), hash-based password comparison (#2022).
Documentation:
- Known security limitations documented (#2078).
- WireBox replaced + TestBox replaced + init decomposed (#1883) — W rim modernization.
application.wirebox→application.wheelsdi(#1888).- Monorepo flattened to clone-and-run structure (#1885).
- CFWheels → Wheels rebrand in active code/metadata (#2064).
- Version bump to 4.0.0-SNAPSHOT (#2066).
- AI infrastructure modernized: 15→5 skills, lean CLAUDE.md, focused commands (#1871).
- Engine-grouped testing (42 jobs → 8) (#1939) — major CI speedup.
- LuCLI-native Lucee 7 + SQLite CI pipeline (#2032).
- Focused Lucee 7 + MySQL test workflow (#1887).
- Workflow results committed back to
claude/*branches (#1892). - Auto-label job fails gracefully on fork PRs (#2007).
- Claude Code runtime artifacts ignored (#2111).
- PR template and Definition of Done (#1918).
Dependabot bumps (dev/CI infrastructure — not user-facing): #1898 (basic-ftp), #1899 (rollup), #1900 (minimatch), #1992 (picomatch), #2020 (vite), #2091 (basic-ftp).
Items that require migration notes for users upgrading from 3.x. These should have top billing in the upgrade guide and blog posts.
wheels snippetscommand renamed towheels generate snippets(#1852) — CLI breaking. Scripts/aliases callingwheels snippetsmust update.- CFWheels → Wheels rebrand in active code (#2064) — callers referencing old namespaces (e.g.,
cfwheels.*) must update. Most user code unaffected; internal reference. testbox→wheelstestnamespace (#1889) — test CFCs should extendwheels.WheelsTest(oldwheels.Testcontinues to work but is legacy).- Tests directory
tests/specs/functions/→tests/specs/functional/(#1872). - Legacy RocketUnit removed from core (#1925) — existing RocketUnit specs in app repos continue to run; WheelsTest (BDD syntax) is mandatory for new tests.
- CORS default: wildcard → deny-all (#2039) — apps relying on the wildcard default must explicitly configure
allowOrigins. allowEnvironmentSwitchViaUrldefault: true → false in production (#2076) — and reload password must be non-empty for env switching in production (#2082).
Additionally, security-hardening defaults (rate limiter trustProxy=false, HSTS default-on in production, CSP/HSTS/Permissions-Policy emitted by SecurityHeaders, CSRF cookie SameSite) may produce visible behavior differences; compatibility-oriented apps may need to tune settings.
The Legacy compatibility adapter (#2015) softens many of these for upgrade.
The [Unreleased] section in CHANGELOG.md currently has 10 Added bullets and no Changed/Deprecated/Removed/Fixed/Security subsections. The following significant user-visible items should be added:
Missing from Added:
- Middleware pipeline (#1924)
- Router modernization (#1891)
- CockroachDB adapter (#1876 + follow-ups)
- Bulk insert/upsert (#2101)
- Polymorphic associations (#2104)
- Advisory locks + SELECT FOR UPDATE (#2103)
- Auto-migrations + rename detection (#2102, #2112)
- Multi-tenant support (#1951)
- HTTP test client (#2099)
- Parallel test runner (#2100)
- Browser testing (Playwright Java) (#2113 + series)
- Pub/sub channels for SSE (#1940)
- Package system / PackageLoader (#1995)
- Module system with dependency graph (#2017)
- LuCLI module distribution (#2018)
- Legacy compatibility adapter (#2015)
- Interface-driven design contracts (#2014)
- Engine adapter modules (#2016)
- XSS helpers / hAttr / stripTags formalized (#2097)
- Security headers middleware (CSP/HSTS/Permissions-Policy) (#2036)
- Debug panel redesign (#2000, #2001)
env()helper (#1985)throwOnColumnNotFoundsetting (#1938)
Missing Changed / Breaking: see Breaking Changes section above.
Missing Security: the 40+ security-hardening PRs deserve a dedicated Security section under 4.0 — see section 19.
Missing Fixed: renderText/renderWith view lookup (#1991), gap migrations (#1928), Adobe Oracle coercion (#2030/#2031), many others.
Recommended: before 4.0 GA, do a CHANGELOG catch-up PR that adds the missing items above under the appropriate Keep-a-Changelog subsections, with PR links.
When blog posts are drafted on top of this audit, these are the natural story arcs:
- "Wheels 4.0: the release that closes the framework-maturity gap" — lead with bulk ops, polymorphic assocs, advisory locks, auto-migrations, browser testing. Pair with updated docs/wheels-vs-frameworks.md.
- "Background jobs without Redis" — the job worker daemon is genuinely differentiated (zero-dependency, DB-backed, multi-tenant aware). Comparable articles in Rails/Laravel/Django presuppose Redis or Celery.
- "Security hardening in 4.0" — 40+ security PRs deserve a dedicated post. Frames Wheels as a secure-by-default framework.
- "From WireBox to wheelsdi: the framework gets leaner" — the internal rim modernization story (decomposed init, WireBox/TestBox replacement, engine adapter modules).
- "LuCLI and the zero-Docker developer experience" — LuCLI adoption arc, phase 1 → phase 4, CI pipeline migration.
- "Upgrading from Wheels 3.x" — practical migration guide centered on the Breaking Changes list and the Legacy compatibility adapter (#2015).
- "Testing in Wheels 4.0" — HTTP test client + parallel runner + browser testing + BDD-only posture.
- "Multi-tenancy built in" — Wheels is now one of the few frameworks with first-class per-request datasource switching.
Between the initial audit (2026-04-16) and the refresh (2026-04-22), 69 additional PRs merged to develop. Bucketed below. The majority were docs-site migration to Astro/Starlight (not framework-surface) and test / CI infrastructure; ~6 were user-visible framework additions.
wheels deploy— Basecamp Kamal port (#2187) — new first-class CLI surface for Dockerized deploys to Linux servers via SSH. Byte-compatible with Kamal'sconfig/deploy.ymlschema and on-server conventions (container names, labels, network, lock path). Invokes the samekamal-proxyGo binary for zero-downtime rollover. Addswheels deploy init | setup | rollback | config | app | proxy | accessory | build | registry | server | prune | lock | secrets | audit | details | remove | docssubcommands. Major addition — warrants its own category in future audits.- SQLite
changeColumnvia recreate-table pattern (#2218) — SQLite adapter previously couldn't alter columns; now supported via table-recreate behind the same migration API. - Vite pipeline: transitive modulepreload + CSS resolution (#2133) — asset-pipeline improvement for the Vite integration (closes part of the "asset-pipeline maturity" gap called out in
docs/wheels-vs-frameworks.md). SecurityHeadersHSTS off-switch (#2195) — explicit opt-out for environments that need to disable HSTS (e.g., behind a TLS-terminating proxy that handles HSTS itself).- LuCLI stdio MCP canonicalized; in-dev-server HTTP MCP deprecated (#2140) — consolidates the MCP surface on
wheels mcp wheels. HTTP endpoint at/wheels/mcpstill works but emits a deprecation warning on first request. Scheduled for removal in a future release. - Framework gap fixes — batch 1 (#2168) — scaffold / routing / forms / CLI polish (umbrella PR; multiple small user-visible improvements).
- CockroachDB bulk-ops + pessimistic locking test failures resolved (#2206) — these features shipped in the initial audit window but had matrix test failures. Now passing across the compat matrix; CockroachDB reaches feature parity for bulk insert/upsert and
.forUpdate().
- CLI:
wheels newnon-zero exit on framework-not-found (#2216) and remaining silent-exit paths (#2221);wheels statscrash + MCP surface curation (#2139); codegen templates bundled into installed wheels-module tar (#2209). - Tests: 20 browser-spec errors resolved (#2134); core test failures across all databases (#2204); dispatch app
populate.cfmacross supported databases (#2198). - zainforbjs: navbar issue #2012 (#2108), issue #2107 (#2109), issue #2166 (#2167), issue #2171 (#2180), issue #2170 (#2172), issue #2202 (#2203), CLI fix (#2199).
- Docs / web:
docstringdefault for@with(#2183, @MukundaKatta — new contributor), various Starlight rendering fixes.
None in this window. #2140 is a deprecation with a warning, not a removal — the endpoint still responds.
- Docs site migration to Astro/Starlight (~25 PRs: #2141, #2143–#2150, #2152–#2154, #2157–#2162, #2169, #2181, #2182, #2185, #2186, #2190, #2191, #2192) — entirely new static-site pipeline replacing the GitBook-era tooling. Major engineering lift, but framework users consume it via browser rather than code.
- Test suite reorganization: move core framework specs from app to core suite (#2200); move browser-test fixtures/routes out of app (#2205).
- CI hardening: visual regression promoted to hard gate (#2163); snapshot API docs deployed on develop (#2164); retire stale workflows (#2165); auto-labeler for fork PRs (#2188); pin verify-docs to node 20 (#2193); node-24 bumps (#2212, #2213); smoke-test installed wheels-module against clean filesystem (#2217); verify-docs
spawn ENOENTroot cause fix (#2210). - Chore:
.gitignore(#2220); delete stalewheels_spec/wheels_build/wheels_validateslash commands (#2151); retire wheels.dev-era publishing pipeline (#2189); drop CommandBox refs from README and CLI docs (#2155, #2156, #2162); renamewheels code→wheels generate snippetsin docs (#2194); 301 redirects for retired CLI URLs (#2197); drop duplicate blog posts + visual baseline refresh (#2191, #2192);.ai/reference sections for MCP and packages (#2142); blog skeletons (#2132) and social announcements (#2137).
- CHANGELOG catch-up PR — add the missing items above under Keep-a-Changelog sections. High priority before GA.
- Vite pipeline maturity — known gap (see
docs/wheels-vs-frameworks.md"Where Wheels Trails"). Candidate for a 4.0.x or 4.1 follow-on spec. Brainstormed 2026-04-16; deferred pending this audit. stripTags/stripLinksencoding default — out of scope for the view auto-encoding discussion; candidate for separate review.- 3.0-vs-4.0 comparison doc — net-new document to produce now that this audit exists; show row-by-row how the framework comparison shifted between 3.0 and 4.0.
- Missing release tags on develop — no
v3.0.0clean tag; onlyv3.0.0+Nbuild markers. A cleanv4.0.0tag at GA time would be clearer. - PR #1891 / #1894 appear to be duplicates (identical titles, same-day) — verify whether both are intentional or whether one should have been closed.