This is the project's CHANGELOG.md, rendered here so the docs and the repository can never drift apart — there is one source of truth, and this page includes it.
Changelog
[4.2.4] — 2026-10-25
Added
- Template (both engines) / Scaffolder: the notification module is now opt-in. A new project has no
src/modules/notification/, nonotificationstable and no/api/notification/*routes;notify()warns once and returnsnulluntil the module is generated. Runmodule:make-notification notificationto write the full backend — model, default storage provider, controller, schemas, routes and mail job — thenmodule:migrate notificationto create the table. The generator prints that migration command when it writes the store; a second, panel-scoped module only adds an HTTP surface over the same store, so it needs no migration. This keeps a permanent table out of projects that never use notifications. - Template (both engines) / Scaffolder: the generated module is standalone. There is no foreign key to
users, no import of@/modules/auth, and no users-model fallback anywhere in the path, so deleting or replacing auth has zero effect on notifications. The table is the module's own:idis aTEXTprimary key holding a UUID generated by the default provider, the owner is a polymorphic(notifiable_type, notifiable_id)pair stored as text, and two indexes cover the read paths —(notifiable_type, notifiable_id, read_at)and(notifiable_type, notifiable_id, created_at). - Template (both engines) / Scaffolder: a
NotificationProvidercontract, so storage is replaceable without touching the runtime. It hascreate,list,unreadCount,markRead,markAllReadandremove, plus an optionaldeleteForTargetfor bulk cleanup.registerNotificationProvider,getNotificationProvider,hasNotificationProviderandclearNotificationProviderare exported from@/framework/facade.js.resolveNotificationProvider()loads provider files and returns the registered implementation ornull; there is no built-in fallback import, so the framework never hard-codes the module. Mail and realtime stay in the framework runtime: a custom provider only replaces storage, sonotify({ broadcast, mail })keeps working. With no provider,notify()warns once (naming both maker commands) and returnsnullinstead of throwing, and the generated HTTP handlers answer503; a missing auth context answers401. - Template (both engines) / Scaffolder: numeric and UUID auth ids are both first-class.
notify(userId, …)acceptsnumber | string, and an explicit{ type, id }target works too; the provider always receives the id as text, so the same call works whether auth uses auto-increment integers or UUIDs. Every id in the auth contract —AuthProviderUser.id,AuthProviderRefreshToken.id,AuthUserContext.id, thegetUserById/getUserContactarguments and the refresh-token operations — is nownumber | string. - Tests (both engines):
notification/__tests__/notification.test.tspins the two guaranteesnotify()makes regardless of provider: a numeric id and a string id both become a stable{ type, id }target (so replacing auth never changes notification code), and a missing provider is warned about once and skipped. The provider suite coversregisterNotificationProvider/getNotificationProvider/hasNotificationProvider/clearNotificationProvider, and the generator suite covers the notification module (full store on first run, store reuse by a panel module,--notifiableplus rejection of an unsafe value).maker-cli/__tests__/db-dialect.test.tsis new and pins SQLite-vs-MySQL migration detection. Hono 352/352 across 28 files, Express 346/346 across 27 files; bothtsc --noEmitclean.
Changed
- Template (both engines) / Scaffolder: new projects no longer ship the notification module or its table. Both
template/{hono,express}/src/modules/notification/directories are gone,src/database/schema.tsexports only the auth models, and the regenerated0000_initmigration plus both prebuilt SQLite databases contain five tables (roles, users and the three auth token tables) with nonotifications. Projects that want notifications generate the module and table with the two commands above. - Template (both engines) / Scaffolder:
module:migrate <module>now regeneratessrc/database/schema.tsfrom every module model, then generates and runs a migration from that full schema. It used to generate from a temporary schema containing only the requested module's models; because Drizzle diffs against the last snapshot, that emittedDROP TABLEfor every other module — auth included — so installing notifications withmodule:migrate notificationcould have dropped the rest of the database. The--keep-tempflag is gone with the temporary schema. The command still verifies that the module exists and owns at least one model before touching migrations. - Template (both engines) / Scaffolder: email recipients are resolved by
mail.tofirst, then the auth provider'sgetUserContact(id), then skipped. The previous third step — reading the defaultusersmodel directly — is gone, because it was the one place the notification path still depended on the auth module existing. A notification whose recipient cannot be resolved still stores and broadcasts; only the email is skipped. - Docs: the notification guide now describes installation as
module:make-notification+module:migrate, the generated file set, the store singleton, the table schema, the opt-in lifecycle, the401/503paths and string notification ids in the UI snippets. The upgrade section for 4.2.3-and-earlier projects replaces the old in-place table rewrite with a drop, backup and copy sequence that matches Drizzle's snapshot flow. The providers guide, the modules guide, the CLI module page and thenotify/getProvider/registerProviderAPI pages no longer claim that notifications ship by default, andmodule:migrateis documented as a full-schema migration. - Template (both engines) / Scaffolder: runtime dependency floors now point at the patched releases:
hono4.13.14 (static-file middleware bypass),express5.3.0 (theqs,body-parserandproxy-addrfixes),@hono/node-server2.1.5,zod4.6.7 (memory exhaustion through oversized constrained collections),vite8.3.5 (launch-editor command injection on Windows),multer2.5.0 (asyncfileFilterdenial of service),express-rate-limit8.7.1,drizzle-orm0.45.4,drizzle-kit0.31.11,socket.io/socket.io-client4.8.4,@vitejs/plugin-vue6.0.10,@asteasolutions/zod-to-openapi8.5.0 and@bull-board/*9.10.5 (express).@hono/zod-openapistays on~1.5.3: 1.6.x pulls@asteasolutions/zod-to-openapi@9, whose peer range conflicts withstoker@2.0.1(^8), and the bump carries no security fix. The Hono@bull-boardpackages stay on 8.6.1 and Hono stays on 4.x for the same reason —hono@5is still a release candidate and@bull-board/hono@9belongs to that migration. No source changes were needed; every bump is a patch or a same-major minor. - Template (both engines) / Scaffolder:
nodemailermoves to^10.2.1, the only line that fixes the SMTP command/cross-domain injection advisory, andmaildevmoves to^3.0.0, whose split@maildev/*packages replace the vulnerabledompurify3.4 chain. MailDev 3.0.0 still asks fornodemailer ^9inside@maildev/smtpandzod ^3inside@maildev/mcp, so the newoverridesblock pins both to the top-level fixed releases (nodemailer ^10.2.1,zod ^4.6.7); the MCP package does not import zod at runtime and@modelcontextprotocol/sdkacceptszod ^4, so the alias is safe. The framework transport only callscreateTransportandsendMail, and MailDev 3 keeps the--smtp/--webflagsmaildev:viewpasses, so both upgrades are drop-in. Smoke-tested on both engines: MailDev 3 starts, serves its web UI with200, accepts a message sent through nodemailer 10, and lists it from/api/email. The scaffolder's optional peers now saynodemailer ^10andmaildev ^3, and itssass-embeddedpeer is replaced bysass, matching what the templates have shipped since 4.1.2. - Dependency note for existing projects: lockfiles are gitignored and never published, so a fresh scaffold already resolved these fixes from the old caret ranges; an existing project keeps whatever its own lockfile pins. To pick the fixes up, copy the changed ranges, the
overridesblock and theallowScriptsblock from the newpackage.jsonand runnpm install. This applies to projects created with 4.2.4 before this refresh, and to 4.2.3 and earlier. Theoverridesblock is the npm/bun field: pnpm 11 reads overrides only frompnpm-workspace.yamland Yarn readsresolutions, so mirror the same entries there if you install with pnpm or Yarn — the direct ranges apply everywhere, but without the mirror the nested@maildev/smtp > nodemailer/jsdom,@esbuild-kit/core-utils > esbuildandredis-commanderpins are skipped. - Template (both engines) / Scaffolder: the engine manifest now declares
"node": ">=22.12.0"instead of>=22, matching the floor the four README copies, the quick-start guide andcreate-nexwire's ownenginesalready stated. With the floor correctly declared, the two Node-gated majors land:vitestand@vitest/coverage-v8move to^5.0.5(Vitest 5 requires^22.12.0 || ^24 || >=26and peers Vite 8), andcommandermoves to^15.0.0(Commander 15 is ESM-only and relies onrequire(esm), available from 22.12). The maker CLI and both CLI test files already importcommanderas ESM and the tests run through Vitest's ESM loader, so the ESM-only switch is transparent; MailDev's own nested Commander 12 is untouched because the overrides only cover itsnodemailerand zod dependencies. Verified on Vitest 5:tsc --noEmitclean, Hono 352/352 and Express 346/346, both coverage runs complete with the v8 provider, andmaker --helprenders on both engines. - Template (both engines) / Scaffolder: the UI's head manager moves from
@vueuse/head@2.0.0— last published in 2023 — to@unhead/vue@3.4.3, the library it wraps.createHeadnow comes from@unhead/vue/clientanduseHeadfrom@unhead/vue; thetitleTemplateand the six static page titles are unchanged. This removes all threeunheadadvisories (theuseHeadSafeXSS bypasses) at the source instead of accepting them as residual, and the production UI build still transforms the same 194 modules. Theentry-point,pulseandnotificationdocs and the scaffolder's optional peer list now reference@unhead/vue. - Template (both engines) / Scaffolder:
package.jsonnow ships anallowScriptsmap, because npm 11.19 blocks dependency lifecycle scripts by default and ends every install with a list of packages whose scripts it skipped. The five name-only entries (@parcel/watcher,esbuild,msgpackr-extract,redis-commander,vue-demi) cover the installed tree, including both esbuild lines currently resolved, so a freshnpm installhas nothing left to review;redis-commander's postinstall is only an informationalecho. Name-only entries survive version bumps, unlike npm's default pinned form. The quick-start's package-manager section documents the equivalent pnpmallowBuildsand YarndependenciesMetalists, and now includesvue-demiin both. - Template (both engines) / Scaffolder: two more
overridesremove the remaining audit findings and one deprecated dependency.@esbuild-kit/core-utils > esbuildis pinned to^0.25.12: that loader is a vestigialdrizzle-kitdependency, it calls only the stabletransform/transformSyncAPI, and the pin dedupes it with drizzle-kit's own esbuild.@maildev/smtp > jsdomis pinned to^27.4.0, the first jsdom line that both supports the Node 22.12 floor and uses@exodus/bytesinstead of the deprecatedwhatwg-encoding. Result on both engines:npm audit(dev included) reports 0 findings,npm install-scripts lsis empty, anddrizzle-kit checkplus the MailDev HTTP/SMTP smoke pass. Four deprecation warnings remain and are upstream-blocked:@esbuild-kit/esm-loaderand@esbuild-kit/core-utils(drizzle-kit 0.31; 1.0 is still RC),cron-parser@4(bullmq 5; bullmq 6 is blocked bybullmq-durable's^5peer) andlodash.isequal(redis-commander 0.9.0).
Fixed
- Template (both engines) / Scaffolder: SQLite migrations were misread as MySQL.
detectMigrationDialectFromSql()tested the MySQL backtick hint before the SQLite hints, and Drizzle quotes SQLite identifiers with backticks too, so every SQLite history was classified as a dialect change and wiped on the nextdb:generateormodule:migrate— then recreated as a new0000_initagainst tables that already existed. The distinctive SQLite hints (autoincrement,pragma) are now checked first. Regression tests cover SQLite, MySQL and Postgres input. - Template (both engines) / Scaffolder:
notify()no longer depends on the default auth module. It resolves storage through the provider contract and the recipient through the auth provider, so replacing or deletingsrc/modules/auth/leaves the notification module untouched instead of breaking the insert path or cascading rows away with the old foreign key. Verified by a test that registers only a fake notification provider and never imports auth. - Template (both engines):
redis:viewno longer pulls the nativebcrypt→@mapbox/node-pre-gyp→tar@6.2.1chain thatnpm auditreports as critical. Aredis-commander-scopedoverridesblock pins its bundled Express and body-parser to the patched 4.x releases and aliases its optional nativebcryptto pure-JSbcryptjs3, which also removes the node-gyp build that needs a compiler on Windows. Redis Commander only callshashSync/compareSync, both present in bcryptjs. Smoke-tested on both engines: the commander starts on a live Redis, serves its UI with200, andrequire("bcrypt")resolves tobcryptjs@3.0.3.
[4.2.3] — 2026-10-22
Added
- Template (both engines) / Scaffolder: a provider registry — the framework's public extension point. Files in
src/providers/(app-wide) andsrc/modules/<module>/providers.ts(module-scoped) are imported automatically at boot, so an implementation is registered simply by dropping a file in place: no edit toapp.ts, no import of framework internals.registerProvider,getProvider,hasProvider,clearProviderandlistProvidersare exported from@/framework/facade.js, together with the typedregisterAuthProvider,getAuthProvider,hasAuthProviderandclearAuthProvider. The facade is the framework's public API;@/framework/...internals are no longer part of the documented surface. - Template (both engines) / Scaffolder: provider-agnostic auth. The auth middleware and the notification mailer ask an auth provider for the user instead of importing
@/modules/auth/*, so the default auth module can be deleted or replaced. A provider may return any user shape — extra fields (tenantId,plan,status, ...) are passed through toc.get("auth")/res.locals.auth— andgetAuthContext()overrides that context entirely when a project needs a different shape. - Template (both engines) / Scaffolder: a module can offer more than one implementation.
src/modules/<module>/providers/*.tsis imported at boot alongsidesrc/modules/<module>/providers.ts, so a module with several providers keeps them in their own folder instead of piling them into one file — the same shape the other module folders already have. - Template (both engines) / Scaffolder: every scaffolded module now gets a
providers.tsnext to itsfacade.ts. It is the place where a module plugs its own implementations into the application, and it is imported automatically at boot, so the module never has to be wired into the app by hand. - Template (both engines) / Scaffolder:
provider:make <name>generates an app-wide provider insrc/providers/, andmodule:make-provider <module> [name]generates one inside a module'sproviders/folder. Both generated files register themselves when they are imported. The two commands are filed in separate help sections on purpose, the waymiddleware:makeandmodule:make-middlewareare: they look interchangeable and are not — one writes for the whole app, the other for a single module.
Changed
- Template (both engines) / Scaffolder:
notify()resolves the email recipient through the auth provider (getUserContact()), falls back to the defaultusersmodel, and accepts an explicitmail.to. When no recipient can be resolved the email is skipped instead of failing the notification, so notifications keep working without the default auth module.
Fixed
- Template (both engines) / Scaffolder: the framework boots safely when optional app modules are missing or replaced. The auth middleware warns and lets the request through when no auth provider is available at all,
@/framework/notificationbecomes a no-op when the notification model is gone, and the seeder skips missing or invalid seeders with[seed:skip]instead of abortingdb:migrate --seed. - Template (both engines) / Scaffolder: the framework no longer imports the generated schema barrel (
src/database/schema.ts). It builds the Drizzle schema from the modules that actually exist, so deleting or replacing a module — the auth module included — no longer breaks startup with a missing-import error. The barrel stays the source for drizzle-kit and is regenerated by the CLI before every migration.
[4.2.2] — 2026-10-08
Added
- Template (both engines): the maker CLI now loads
maker/commands.tsas well asmaker/commands.mjs, so a project's own commands can be written in TypeScript and import project modules through@/— replacing the wrapper pattern the scaffold itself used to recommend: a thin.mjsfile whose only job was tospawn("npx", ["tsx", ...])the real logic back out to a child process, which loses exit codes, doubles startup, and leaves the wrapper and the script it wraps free to drift apart. When the resolved project file ends in.ts, the loader registers tsx's ESM hooks in-process viatsx/esm/apibefore importing it, with no options so tsx discovers the project'stsconfig.jsonfrom the cwd — which is where thepathsmapping lives — guarded by aglobalThis.__nexwireMakerTsxRegisteredflag because registration is process-wide: two overlappingregister()calls corrupt the loader (proven with a probe; a module-level flag is not enough for a test runner that re-evaluates the module inside one process), and the registration is deliberately never undone because a command's action canawait import("@/…")after dispatch, when the hooks must still be alive. The.mjspath never imports tsx at all — byte-identical to what shipped before — so projects that use plain JavaScript pay nothing for this and production installs without devDependencies are unaffected, tsx being a devDependency. If both files exist,maker/commands.mjswins: it is the incumbent and an existing project must not break, and the CLI prints a warning naming the ignored.tsfile rather than leaving it silently dead — the same "ignored, and you are told" rule the framework applies to file collisions elsewhere. Verified in two real scaffolded projects, not only the template suite: game-ho'smaker/commands.mjs— arunTsxwrapper whose only job was to re-launchnpx tsxon the real CLI in a child process — became amaker/commands.tsthat importsrunSimulationandrunCertificationfromsrc/through@/and calls them directly, so the wrapper is gone:maker simulate dice --spins 1000andmaker certifyboth dispatch in-process and print their full reports, the CLI scripts still run standalone through an invoked-directly guard, andtsc(root and maker configs), biome and the project's 134/134 tests are all clean; game-ex, which had no project commands at all, gained amaker/commands.tswhose action dispatched through@/(134/134,tscclean), and the both-files rule was observed live there —maker/commands.ts is ignored because maker/commands.mjs exists. Keep one - delete the file you are not using., the.mjscommand ran, the.tsone was absent, and deleting the.mjsrestored the.ts. The template suites cover the same contract: 6 new tests per engine, each proven red before it went green — the five loader tests against the old loader (missing export,loaded:falsefor a throwing.ts, no warning for both files) and the sixth by removingmaker/tsconfig.json— and green after: hono 326/326, express 320/320, bothtsc-clean. A real project is what made the last piece necessary: a file no tsconfig claims falls into TypeScript's inferred project, andmaker/commands.tsshowed phantom errors fornode:child_processand every@/import — the editor equivalent of the feature simply not being there.maker/must stay outside the rootinclude, because adding it changes the common source directory and would relocate every emitted file underdist/, so the scaffold shipsmaker/tsconfig.jsoninstead: extends the root config,noEmit,rootDir: "..", includemaker/**/*.ts. The editor picks that config up for anything undermaker/and the reds disappear (tsc -p maker/tsconfig.jsonexits 0 on both real projects), whilebuild:backend'stsc -p tsconfig.jsonnever reads it anddist/is untouched.
Changed
- Template (both engines): the
maker/commands.mjstemplate written bymaker:inittold developers to keep "thin .mjs files" that import project code by relative path — advice that was already wrong for any TypeScript source, and the reason projects grew spawn wrappers like game-ho's. The comment now says the file is plain JavaScript under plain Node, cannot import TypeScript source or a tsconfig path alias, and that a command needing either should name the filemaker/commands.tsinstead — nexwire loads that through tsx with the project's tsconfig applied.maker:inititself still creates the.mjs, so nothing about an existing workflow changes.
[4.2.1] — 2026-10-08
Fixed
- Template (both engines):
group(middleware)ran for routes it was never declared on. It registered the middleware as a catch-all on the router —use("*", ...)on hono,use(...)on express — which is a route entry rather than a property of the routes being grouped, and route entries are what a mount copies. A module that exports a public group and a protected group under the same prefix, which is the documented pattern, therefore shared middleware across them: a request to a protected route also ran the public group's rate limiter, because the limiter was registered first and matched everything entering the parent. Measured directly rather than inferred — a probe against the old code recorded["limiter:/mod/private","auth:/mod/private"]for a route that only ever declaredauth. The same catch-all reached beyond the group altogether: add any route to the parent after mounting the group and the group's middleware ran for it too, so a login rate limiter could start throttling an unrelated endpoint.group()now records its middlewares on the router and merges them into eachapi()call, so they travel with the routes they were declared on and reach nothing else. Ordering within a route keeps its one guarantee — group middleware still runs before the middlewares passed toapi()— but its position relative to validation is now identical on both engines, and that is a change on express: a router-leveluse()sits ahead of every route in the stack, so express previously ran group middleware before the route's validation middleware, whereas hono has always validated first because validation happens before the wrapped handler runs. express now validates first too, so a body or query that fails validation is rejected before group middleware runs. That matters for anything rate-limiting or authenticating declared withgroup(): it now counts only requests that passed validation on express, which is what hono has always done. The one narrowing is deliberate and matches what the documentation already said: group middleware applies to the.api()routes on the router it was declared on, not to aapp.get(...)added to the parent or to a sub-router mounted underneath it. - Template (both engines): the same catch-all could make a route answer with another route file's rejection. Route files in one module are all mounted at the same
/api/<module>prefix, anddiscoverModuleFilesmounts them in sorted order, so agroup(authMiddleware)declared inadmin.tswas registered asALL /api/<module>/*beforeruntime.ts's routes existed. A request to a runtime route was therefore answered by that module's cookie auth with{"message":"Unauthorized"}401 — a middleware the route never declared — and the route's own group never ran. Seen in a real project:game-suite/routes/admin.tsandgame-suite/routes/runtime.tsare both mounted at/api/game-suite, soPOST /api/game-suite/v1/spincould not reachgroup(hmacAuth). Verified on a running app after the fix:GET /api/game-suite/gamesstill returns{"message":"Unauthorized"}(its own group), whilePOST /api/game-suite/v1/spinnow reacheshmacAuthand returns{"error":"INVALID_API_KEY"}for a bad signature. - Template (hono):
.api(route, undefined)did not throw. hono took the two-argument branch and handed the missing handler straight toopenapi(), so the mistake was accepted at registration time instead of failing where it was made; express already threwapi(route, middlewares, handler) requires a handler function. hono now throws the same message, so the two engines fail identically on the same typo. - Template (both engines): the published
vitest.config.tsclaimed the coverage floor "is applied by CI" — but that workflow was removed in 4.2.0, so every scaffolded project shipped a comment pointing at a workflow that does not exist. It also named the floor as14%, which is no longer the floor. The comment now says the floor is passed on the command line when the framework's own suite is run and is never read from that file, and it drops the number rather than carrying one that has to be updated every time the floor moves. Verified byte-identical across engines (vitest.config.tshashes equal on hono and express). - Docs: the Testing guide still said the floor is kept "in its own CI", quoted the retired
14%, and describedtest:coverageas reporting "with a floor" in a table whose section explains there is no threshold; the CLI reference saidtest:watch"does not finish in CI and is not wired into the test workflow". All three now describe local, command-line thresholds, andtest:runis labelled "no watch mode" rather than "CI mode".
Changed
- Template (both engines): both seeder shapes now build their completion log line with a template literal instead of string concatenation. The words and the printed output are unchanged — only the generated source differs, and hono and express stay byte-identical to each other and to the copies in the scaffolder.
Added
- Tests (both engines):
http/__tests__/router-group.test.tspins the scoping contract behind thegroup()fix, and was written before it: 10 tests on hono, 9 on express (express'screateRouter()already rejected a missing handler, so that one assertion was never red on express). Every scoping test was run against the old implementation first and left failing until the fix landed — on the pre-fix code 4 of 10 fail on hono and 3 of 9 on express, including the case that produced a real bug report: a sibling group's rejecting middleware blocking a route it was never declared on. They cover the leak itself (a request to the protected group must not run the public group's limiter, at any mount depth), the same leak onto a route added to the parent after the mount, a rejecting sibling group, group-before-route ordering, repeated.group()calls accumulating, a middleware that does not callnext()short-circuiting the handler, and thegroup()shorthand. The hono test drives real requests throughapp.request(...)and the express test through a listening server, so both assert on what a client would actually observe rather than on internal registration order. Both engines stay above the floor with the new tests: 33.46% / 25% / 29.72% / 34.14% (hono) and 32.10% / 24.73% / 29.9% / 32.61% (express).
[4.2.0] - 2026-10-06
Fixed
- Template (both engines): A multi-word module, model or controller name produced code that does not compile. The generators wrote the name into identifier positions exactly as typed, so
module:make game-suiteemittedexport const game-suites = sqliteTable("game-suites", {andimport { game-suites }. JavaScript readsgame-suitesas subtraction, somodule:make-model,module:make-seeder,module:make-service,module:make-controllerandmodule:make-helpereach produced files that fail to compile — while single-word names worked, which is why this went unnoticed. Names are now normalised to one canonical kebab form and the four shapes each placeholder needs are derived from it: kebab-case for file names and import paths, camelCase for the service and table identifiers (gameSuiteService,gameSuites), snake_case for the SQL table (game_suites) and PascalCase for class and schema names (GameConfigSeeder). The regression is covered by a test that generates every module generator into a temp directory and parses the output with the TypeScript compiler, so a leak fails the build rather than producing a broken file. - Template (both engines): Names are normalised before they are validated, so every casing a developer might type converges on one file:
game-config,game_config,gameSuite,GameConfigandGAME-SUITEall producegame-config.ts. PreviouslygameConfigandGameConfigwere lowercased togameconfigbefore any word boundary could be read from them, so the boundary was destroyed. A word written without a separator carries no boundary to recover, sogamesuiteis kept as one word rather than guessing wheregameends; a capital inside a word is a boundary, sogamE-SUITE-Enginebecomesgam-e-suite-engine. Any casing of the same words resolves back to the module that was already created. - Template (both engines):
module:make-helpernamed its exported function after the raw input (export function game-configHelper<T>), which did not compile. It now uses the PascalCase form, matchingmodule:make-typeandmodule:make-middleware. - Template (both engines): A module folder scaffolded before this release under a different separator (
game_configrather thangame-config) is no longer reachable, because the name is now canonicalised before the folder is resolved.assertModuleExistsfalls back to a separator-insensitive lookup, so existing modules keep working and are never renamed. - Template (both engines):
queueJobsilently discarded work when Redis was not available. It returnednullwith no output, so a caller such asawait dispatchEvent("orders.exported", data, { queue: true })lost the job and had no way to learn it had been lost - and because a freshly scaffolded project ships withREDIS=false, this was the default path rather than an edge case. It now warns once per queue and job name, naming the job, its queue, a scheduled delay if one was given, and both ways forward: enableREDISin.env, or calldispatchCommand(name, payload)without{ async: true }to run the work in-process. The return value is stillnull- there is genuinely nowhere to put the job - but the loss is now reported, and it is reported once per job so a request hot path cannot flood the log.startQueueWorkercontinues to throw, since a worker without Redis has nothing to poll. - Template (both engines): A queue worker leaked one Redis connection on every restart.
stopQueueRuntimeset its cachedRedisStateStoretonullwithout ever callingclose()on it — and that store opens a Redis client of its own the first time a durable job runs, because bullmq-durable does this deliberately so that merely constructing a store costs no connection. The framework's own comment on that function claimed the store reused the shared client "without opening extra connections", which is why the leak survived review. Nothing ever quit those connections, and because the module-level reference was nulled rather than the store closed, the next boot built a new store and opened another. A worker restarted ten times by its supervisor left ten orphaned connections behind, and the failure surfaces on an unrelated service first:OOM command not allowed, or connection-refused from the web tier minutes after a routine queue restart, with nothing pointing at a leaked socket.stopQueueRuntimenow closes the store before dropping it, and ten boot/work/stop cycles leave behind exactly as much as one. - Template (both engines): Starting the queue runtime twice for the same queue opened two BullMQ workers for it. Only the event streams were de-duplicated, not the workers, and each
Workerholds its own blocking Redis connection — so a second boot silently doubled that connection and doubled every per-job log line while processing nothing extra, because BullMQ hands each job to exactly one of them. The likely trigger is a supervisor retry or a dev server that boots the worker on reload.stopQueueRuntimeclears the guard, so a later boot in the same process still works. - Template (both engines):
maker teststarted Vitest in watch mode and so never exited. It ranvitest --coverage, and without therunsubcommand that is watch mode: the command hung until interrupted and reported nothing. A script a developer or a CI job reaches for to get an answer instead handed back a terminal to Ctrl-C.testandtest:coveragenow passrun;test:watchstill watches, which is its entire purpose. - Docs: The Testing section of the CLI reference documented
npm run maker test -- -- --filter=postsas the way to run part of the suite. Two things were wrong with it. Vitest has no--filteroption, so it stopped withCACError: Unknown option \--filter`; and the second--arrives at Vitest as its own argument separator, which it reads as a filter matching nothing — so the command appeared to succeed while quietly running the whole suite. Filtering is positional (maker test posts`), and the reference now says so in a tip, explains why, and shows the npm form that needs one separator rather than two.
Changed
- Template (both engines):
controller/openapi.with-model.ts.stubhas been removed. It was byte-identical tocontroller/openapi.ts.stuband both were added in the same commit, so theopenapiWithModelentry in the stub map was a duplicate with no distinct content.module:makenow selectsopenapi.ts.stub, and the never-readopenapiWithModelentry is gone from the deploy stub map too. Generated output is unchanged - verified by hashing the controller thatmodule:makewrites for both route styles, before and after, on both engines. Note that--with-modelis unaffected: it selects the Drizzle-backed service stub, which is a genuinely different file. - Scaffolder packaging: The maker-cli's own tests (
src/framework/maker-cli/__tests__/) are no longer published. They cover the generators, not the application, and a developer writing their own tests inside their modules would otherwise find two unexplained files in a directory they never opted into — which reads as something they are expected to look after. The tests are kept in the repository so the generators stay covered, andscripts/sync-template.mjsskips__tests__undersrc/frameworkwhen copying a template into the published package. Tests inside modules (src/modules/*/__tests__/) still ship, since those belong to the module being used and show the pattern for it. Nothing else about a new project changes except that its test count is 64 lower.
Added
CI: The engine test suites used to run on every push and pull request (
.github/workflows/test.yml, a matrix over hono and express on Node 22). That workflow has been removed: it kept failing on an npm 11"edgesOut"cache bug that did not affect tests themselves, and GitHub Actions runners began emitting Node-20 deprecation warnings for every step. Local test discipline is now the source of truth — the entry below explains the coverage floor and the command to run locally. No change to what tests do; every suite is still green.Tests (both engines): A coverage floor, passed on the command line rather than written into
vitest.config.tsso that the published file does not impose it on every scaffolded project. It is a floor and not a target, set just under the current measurement, and verified to bite by raising it and watching the run fail. The starting point was measured for the first time: 9.86% of 2,190 statements, with 58 of 91 files at literally nothing. That number is unflattering and is the reason this entry exists - the suite was a map of the bugs already hit, because tests were written at the moment a bug was found rather than as a strategy, so everything nobody had tripped over yet was at zero. The floor is set by whichever engine measures lower (express at 30.26% against hono's 32.08%, because it carries engine-specific files hono does not); setting it from hono would set express red, which is exactly the day-one breakage the floor exists to prevent. Runnpm run test:run -- --coverage --coverage.thresholds.statements=30 --coverage.thresholds.branches=23 --coverage.thresholds.functions=27 --coverage.thresholds.lines=30locally — the same threshold the old CI step enforces.Template (both engines):
bun.lockbis now gitignored alongsidebun.lock,package-lock.json,pnpm-lock.yamlandyarn.lock. It was the one lockfile format that was not ignored, so a developer working with bun before 1.2 would see an untracked binary lockfile appear in their working tree and have to decide what to do with it. That is not a decision a developer's package manager should be silently asking them to make on every upgrade, and the templates already take the position that each package manager regenerates its own lockfile.Tests (both engines): The first tranche of resource-lifecycle tests, covering the failure class a coverage number cannot see.
redis/client.tswent from 0% to 73% andscheduler/lock.tsfrom 0% to 80%, taking framework coverage from 9.86% to 14.74% and the suite from 180 to 214 tests. These count what coverage cannot:initRedis()called ten times must open one connection carrying one error listener, becauseclient.tsis a module-level singleton and nothing in a type signature stops server, worker and scheduler bootstrap from each opening a socket;closeRedis()must actually release it, must fall back to a hard disconnect whenquit()fails (which is precisely when you are usually shutting down), and must be safe to call twice or with no connection at all; and a failed handshake must close the half-open socket rather than only dropping the reference, since that happens on every retry. The scheduler lock is the same shape with a worse failure: a lock not released when the handler throws means the job silently stops running forever while still reporting success and logging nothing. Both backends are pinned for that, and all three SQL dialects, because a lock only ever exercised against SQLite - which is all a developer running the suite locally will do - can leave the Postgres and MySQL branches broken until deployment. Redis log output is checked for password redaction, since those lines end up pasted into issues.Template (both engines):
maker --helpnow opens with an ASCIInexwirewordmark, the way developer tools commonly brand their own help. The art is a fixed literal inutils/help.mjs; figlet was used offline to design it and is deliberately not a dependency, because the maker CLI runs from source inside the user's project and a runtime figlet would mean every scaffolded project installs it - font data and all - purely to print five fixed lines. Nothing is computed at runtime. Three rules keep the banner from getting in a developer's way, since decoration that interrupts is worse than no decoration: a terminal narrower than 41 columns gets plainnexwire makertext, because art that overflows wraps into an unreadable mess; piped or redirected output gets plain text, somaker --help > help.txtproduces a readable file rather than one that opens with ASCII art; and per-command help gets no wordmark at all, becausemaker module:make --helpis for checking a flag, not for branding. Verified at simulated terminal widths 30, 41, 45 and 100, and against genuinely piped output.Template (both engines):
maker --helpis now grouped by intent instead of listing all 57 commands in one flat alphabetical wall, where findingqueue:workmeant scrolling past forty unrelated names. The sections are Getting started, Generators - global, Generators - modules, Modules - manage, Database, Queue and scheduler, Deploy, Testing, and Your commands. Group membership is an explicit list rather than a prefix pattern, because amodule:make-*pattern would also have swallowedmodule:migrateandmodule:make-notification, and a command filed under the wrong heading is worse than an extra line of configuration.middleware:makeis deliberately in its own section frommodule:make-middleware: the first writessrc/middlewaresfor the whole app, the second writes inside one module, and listing them together implies they are interchangeable. The listing shows names and descriptions only - commander's default added[options] <module> <name>markers that pushed the description column to 47 characters and left almost no room to say what anything did; options and arguments remain one command away viamaker <command> --help, which is unchanged and still uses commander's own layout. Any command not named in the group map falls into anOther commandssection rather than disappearing, and__tests__/help.test.tsbuilds the real program from the five real registrars and fails if any command is unclassified - so adding a command without deciding where it belongs is a test failure rather than an invisible omission. That test caught a genuine regression while this was being written: commander adds itshelp [command]lazily, after the formatter has run, so the command stopped appearing in the listing. It still works, and both spellings are now named in the footer rather than a working command being quietly dropped.Docs and wording: every reference to another PHP framework has been removed from the repository - the docs, the changelog, the CLI help text, the source comments and the tests. This is a TypeScript framework with its own conventions, and borrowing another project's vocabulary invited exactly the wrong comparison. Reworded rather than simply deleted wherever the sentence needed to say why: pagination is now described as paginating Drizzle relational queries, Drizzle Kit is described as not supporting rollback or refresh operations, and a seeder is described as a function that inserts rows with the developer choosing what it writes.
Docs: The Testing guide documents coverage thresholds - why a new project ships without one (its number is mostly framework code the developer did not write, so a floor calibrated for this repository would fail a fresh install with every one of its tests passing), how to add one that is worth having, that raising it belongs in the same change as the tests that earned it, and that lowering it is the one edit that quietly removes the protection. It also says plainly what a coverage number cannot find, because that is the more useful half: a leaked resource where every line is covered, state that survives a request, and a wrong answer on an unusual input are all invisible to a percentage. The guide's
vitest.config.tssample was stale and now matches the shipped file.Template (both engines): A project can now add its own maker commands.
maker maker:initcreatesmaker/commands.mjswith a working example, and whatever that file exports is registered on the CLI alongside the built-ins. It receives the same commanderprogramobject the built-in registrars get, so.command(),.description(),.option()and.action()behave identically. The file sits in the project root rather than undersrc/because everything undersrc/framework/is copied into the published package - a developer edit there would be overwritten by the next framework update - andsrc/modules/is swept by the route, job and seeder globs. It is resolved fromprocess.cwd(), notimport.meta.dirname, which points insidenode_moduleswhen the package is installed. Absent is silent and free, since most projects will not have the file. Broken is loud and never swallowed: an unparseable file, one that throws while loading, and one exporting the wrong shape each stop the CLI with an error namingmaker/commands.mjsand a non-zero exit, because a developer's commands silently not existing sends them debugging their own command instead of the loader. Framework names win - commander refuses a duplicate, so a project cannot shadow a built-in - and the resulting error names both the file and the taken command. Custom generator stubs are deliberately not supported:module:make-*reads from the framework-owned stub directory, and having a custom command write its own file is the honest trade.Template (both engines):
module:make-seederno longer refuses to generate anything when no model matches the requested name. It used to throwModel not found for seeder: ..., which made three ordinary requests impossible: seeding a table owned by another module, seeding rows assembled from several models, and seeding a table whose model does not exist yet. A missing model now yields a standalone seeder that imports no model and inserts nothing until you fill it in, and the CLI prints what it found and what to do next — including which models the module does have. When a matching model does exist the seeder is unchanged in behaviour: the model is imported,tableis exported and the insert loop is pre-filled. Its completion log now reports the row count (GameConfig: 3 row(s) prepared) instead of a bareGameConfig seeder completed, so both seeder shapes report the same way and an empty seeder is visibly empty. A seeder never needed a model to begin with: it is a function that inserts rows, and what it inserts is the developer's choice - and every other generator in this framework already degrades to a no-dependency shape when its dependency is missing - a controller without a service, a service without a model. Seeder was the only one that refused. The file lives in the module you named, somodule:seed <module>still runs it.Tests:
template/*/src/framework/maker-cli/__tests__/naming.test.tspins the naming contract — normalisation, idempotency (re-running never churns an existing name), the SQL/pluralisation rules, and every real identifier a project already uses.generators.test.tsdrives all ten module generators for four input casings and asserts the output parses and contains no kebab in code, and pins the seeder's bound and standalone shapes.Docs: Added a Naming section to the Module Commands reference with the four derived forms, a normalisation table, the pluralisation rules, and the regeneration steps for projects generated before this release. The
module:make-seederentry now documents both seeder shapes and when each is chosen.Tests (both engines): The second tranche of resource-lifecycle tests, taking framework coverage from 14.74% to 22.16% (hono) and 14.25% to 21.24% (express), and the suite from 214 to 258 tests.
queue/queue.tswent from 0% to 91% andrealtime/socket.tsto 84% (hono) / 95% (express). These found two more leaks of the same shape as the first tranche, which is the argument for writing them at all. The durable state store was never closed:stopQueueRuntimenulled its cachedRedisStateStorewithout callingclose(), and that store owns a Redis client of its own, so a worker restart leaked a connection permanently — see the Fixed entry. A second worker was opened for a queue already being worked, doubling a blocking Redis connection because only the event streams were de-duplicated. The socket tests found the same adapter-connection lifecycle on the realtime side and pinned it too:initRealtimecalled three times must return one server,closeRealtimemust release the connectionattachRedisAdapterduplicated and fall back to a hard disconnect whenquit()fails, and ten boot/serve/shutdown cycles must leave behind exactly as much as one. Beyond leaks, the queue tests pin the things that lose work quietly rather than loudly: a worker that refuses to start without Redis must not leave a half-built worker behind, a job arriving with no handler must be rejected naming the queue and job (No handler registered for default:mystery.job) rather than failing with something generic, handlers are keyedqueue:jobso a same-named job on another queue cannot borrow the wrong handler, and afailedevent with a null job must not throw inside the listener and take the emitter down with it. The prune tests pin the destructive path, becausemaker queue:workdeletes queues: a configured queue is never touched, a queue the worker was launched with is never touched, a stale queue still holding a live job is left alone,AUTO_PRUNE_QUEUES=falsedoes not even look, and a Redis that refuses the scan leaves workers starting.clearQueueis pinned the same way — it wipes the queue and durable key spaces and nothing else, because widening the pattern would take the cache, sessions and rate-limit counters with it and log every user out.Tests (both engines): The coverage floor moves from 14/11/9/14 to 30/23/27/30 (statements/branches/functions/lines). It is no longer enforced by GitHub Actions (that workflow has been removed); run
npm run test:run -- --coverage --coverage.thresholds.statements=30 --coverage.thresholds.branches=23 --coverage.thresholds.functions=27 --coverage.thresholds.lines=30locally. Set from express again, since it still measures lower than hono — 30.26% against 32.08% statements — because it carries engine-specific files hono does not; setting the floor from hono fails express, which is the day-one breakage the floor exists to prevent.Tests (both engines): The third tranche, taking framework coverage from 22.16% to 32.08% (hono) and the suite from 258 to 310 tests (hono / 305 express). Tier 2:
http/ratelimiter.ts(5 tests, hono-only) confirms the Redis failover store preserves per-key isolation — a user whose Redis call fails must not see another user's rate-limit bucket, and the 30-second cooldown must expire before Redis is re-attempted.session.ts(8 tests) pins that each session id gets its own document, one id'ssetdoes not affect another,destroyremoves only the target, and all operations degrade gracefully when Redis is unavailable. Tier 3:paginate.ts(13 tests) pins the page math with a fake query fixture — first/middle/last page offsets, page clamping pastlastPage, zero-page clamping,perPagecapping atmaxPerPage, zero-total nulls, and the URL assembly rules (path with existing query string appends&page=not?page=).storage.tsstream lifecycle tests pin thatputon local driver completes mkdir then writeFile, mkdir failure leaves no file on disk, S3sendrejects cleanly,PassThrougherror handler is registered on every S3 upload, andconsumeGeneratedis atomic (read fails → delete must not run).queue/ui.tspoll-interval tests pin that one interval is created on setup,stopQueueDashboardclears it, and double-setup does not double the interval.
[4.1.2] — 2026-10-05
Fixed
- Template (both engines): The UI failed to start on a fresh scaffold with
[sass] spawn UNKNOWN. Vite resolves its Sass compiler by tryingsass-embeddedfirst, and the template depended on it explicitly, which downloads a nativedart-sassexecutable intonode_modules. That binary is blocked by Windows Application Control / AppLocker and some antivirus setups, sonpm run devdied before the UI could compile. The template now depends on the pure-JSsasspackage instead;sass-embeddedis an optional peer dependency of Vite, so once it is absent Vite falls back tosassand no executable is needed. Verified by removingsass-embeddedfromnode_modulesand runningbuild:uion both engines - the UI compiles. - Release tooling:
sync:hono/sync:expressnow pinREDIS=falseon every synced env file, so the flag can no longer be flipped by a local, gitignoredtemplate/*/.env.REDIS=trueshipped in 4.1.0 (requiring the 4.1.1 patch) because the template.envis developer-local and unreviewed while the syncedpackages/create-nexwire/*/.envcopy is tracked and published. - Release tooling: The sync now reports any key where the synced
.envdisagrees with the tracked.env.example([hono] .env differs from .env.example — this ships: SOCKET: false -> true), so unreviewed drift is visible at sync time instead of after publishing. No default is changed by this; it only surfaces the difference.
Added
- Docs: Added a Sass section to the Vite configuration guide explaining why the project uses pure-JS
sass, and how to fixspawn UNKNOWNon an existing project.
[4.1.1] — 2026-10-04
Fixed
- Template (both engines): the published
.envshippedREDIS=true, which made scaffolded projects expect a Redis server on first run. Changed toREDIS=false— the same value the tracked.env.exampleand theREDISschema default already used. 4.1.1 differs from 4.1.0 only inhono/.env,express/.env, and the version.
[4.1.0] — 2026-10-04
Added
- Maker CLI: Added module:make-schema, module:make-service, module:make-helper, module:make-middleware, module:make-type subcommands.
- Maker CLI: module:make supports variadic names and --path for panels. here panel is a separator for api.
- Maker CLI: module:make-notification/module:delete-notification accept --path.
- Framework: cache.set() and session.set() with put() as deprecated aliases.
Changed
- Maker CLI: Stricter name validation (lowercase start, no leading digits) with suggestions.
- Maker CLI: Batch continues on failures, exits nonzero; existing-module guard with --force.
- Maker CLI: writeFiles guards against overwriting without --force.
- Framework: Express session uses Express APIs.
- Docs: Rewrote the Modules guide for the 4.1.0 module layout — the new
schemas/,services/,helpers/,middlewares/andtypes/folders, thefacade.tspublic API, panels (--path), and the fullmodule:make-*command set. Corrected the documentedOPEN_APIdefault (it istrue, notfalse) and the plain-controller import paths (now@/modules/<m>/schemas/<name>.js, not a relative./<name>.schema.js). - Docs: Corrected the Module Commands reference to match the code —
module:makenow lists thefacade.tsandservices/outputs with current paths (no more.controller.ts/.schema.tssuffixes orroutes/api.ts),module:make-controllerno longer claims to generate a schema file, and the missingmodule:make-service,module:make-helper,module:make-middlewareandmodule:make-typesections were added. Removed the leftovermodule:exampleparagraph (command was deleted in 4.1.0). - Docs: Added a Panels section (
--path=<panel>) and a Removed Commands note to the Module Commands reference; fixed the CLI reference command-category count (20 module commands, was 16). - Docs: Corrected the module layout tree in all six README copies (root, create-nexwire, and the four template/scaffolded copies) — added
__tests__/,helpers/,middlewares/,schemas/,services/,types/andfacade.ts, and fixed__test__/→__tests__/.
Removed
- Maker CLI: Removed module:example command and stubs.
Fixed
- Maker CLI: Panel-aware operations, seeder fallback, and migration temp names corrected.
[4.0.0] — 2026-09-28
Successor to
create-nexgen@3.2.1. The old package stays published and must be deprecated — do not delete it, because existing installs would hard-fail. Deprecate it with:npm deprecate create-nexgen "Renamed to nexwire. Use: npm create nexwire@latest my-app"
Breaking
Project renamed to
nexwire— the unscoped npm namenexgenis owned by an unrelated package (samwalshnz, "Get the current song on a radio station", 2022), which made the framework effectively undiscoverable: searching "nexgen" surfaced that package, andnpx nexgen@latestexecuted its binary rather than the scaffolder. The new name is uncontested —nexwire,create-nexwireand the@nexwirescope all return 0 registry hits. 697 brand references across 206 files: root packagenexwire-monorepo, scaffoldercreate-nexwire, directorypackages/create-nexgen/→packages/create-nexwire/, templatesnexwire-express/nexwire-hono, and theNexgenRoutertype →NexwireRouter. Install command is nownpm create nexwire@latest my-app; the published bin also exposes the barenexwirecommand.NEXGEN_*environment variables renamed toNEXWIRE_*— 29 tokens across templates, scaffolder,.envfiles and docs (e.g.NEXGEN_FRONTEND_URL→NEXWIRE_FRONTEND_URL). This breaks any existing deployment — update your.envand deploy environment. Projects scaffolded fromcreate-nexgen@3.2.1still use theNEXGEN_prefix and are unaffected, since the framework is copied into the project at scaffold time rather than installed as a dependency.GitHub URLs updated to
/nexwire(the repository rename itself is still pending and must be performed for these links to resolve) — allgithub.com/niyamulahsan/nexgenURLs now point to/nexwire.
Added
Demo recordings in the showcase READMEs — the READMEs asserted that the framework works (wiring, dashboards, deploys) but contained no visual proof, only tables and tree diagrams. Two real screen recordings now ship in
packages/docs/public/screenshots/and are embedded in the two showcase READMEs (rootREADME.mdandpackages/create-nexwire/README.md) under a new## Demosection — screen recordings rather than stills, because the claim being made is about a sequence (one command bringing a stack up, then the same project surviving a real deploy), which a screenshot cannot carry.dev-mode.gifis a freshly scaffolded project with its dev stack booting;real-deploy.gifis a real application — not a demo — deployed to a live server with HTTPS, process supervision and zero-downtime restarts. The two clips deliberately show different applications, because they are proving different things: the first that a generated project runs, the second that the framework is not a toy. Both are genuine captures of a running app: no mockups, no design tool, no fabricated states. The files live in the VitePress public directory rather than a new top-levelassets/folder, so the docs site serves them at/screenshots/*.gifas well, and the READMEs reference them by absoluteraw.githubusercontent.comURLs (the same pattern the logo badge already uses) so they resolve as soon as the files reachmain, without waiting on a Pages deploy, and one committed copy serves both the GitHub page and the npm page. The scaffolded project READMEs are deliberately left unwired, since those files ship neither the recordings nor the docs site.Encoding:
dev-mode.gifis built from a 64.47s 1918x1144 30fps capture, downscaled to 959x572 (scale=959:-1:flags=lanczos) at 15fps rather than dropped further, because terminal text blurs if the width goes below native — frame rate is the cheaper axis to spend on a screen recording than resolution — then a 256-colourpalettegen/paletteusepass (dither=sierra2_4a,diff_mode=rectangle) andgifsicle -O3 --lossy=100(6.69 MB; the lossy pass is what actually shrinks a GIF,-O2/-O3alone barely move the number). Both files loop infinitely. Duration, resolution and frame content were verified after encoding rather than assumed, and six frames sampled acrossdev-mode.gifwere confirmed to carry real screen content (74–157 KB per extracted PNG, against ~3 KB for a blank 959x572 frame).Note for anyone re-cutting these: an earlier
dev-mode.gifwas built from a superseded 27.93s recording, and an even earlier one had been recorded against a project usingpostgres://localhost:5432/statisticinstead of the shipped SQLite — which directly contradicted the README's zero-setup claim. The clip is only honest if the terminal it records shows the SQLite default that actually ships.Changelog published as a docs page — the docs site documented every subsystem except the one that answers "what changed between releases", and the release history lived only in the repository, which is invisible to anyone reading the docs. A new
packages/docs/changelog.mdrenders at/changelog, linked from both the top nav and the sidebar, so the history is readable without leaving the site. The page uses VitePress@includeto pull in the repository'sCHANGELOG.mdrather than holding a copy, so the public changelog and the repository changelog cannot drift apart — one file to edit per release, which is the only way a published changelog survives contact with a busy release.
Fixed
The scaffolder's template sync never ran at publish time —
create-nexwiredeclared its sync asprepublish, a lifecycle hook modern npm (>=7) no longer runs fornpm publish, soscripts/sync-template.mjssilently never executed and the publishedhono/+express/copies were whatever happened to be on disk. Renamed toprepublishOnly, the hook that npm actually fires on publish. Verified with a probe package:npm publish --dry-runrunsprepublishOnlyand notprepublish.create-nexwire@latest .could delete the directory it was run in — on a failed template download the error path ranrmSync(targetDir, { recursive: true, force: true }), but with the.argumenttargetDirisprocess.cwd(), so the cleanup removed the user's own project (including.git). Cleanup is now skipped when scaffolding into the current directory, and the failure message says so. Windows masked this by throwingEPERMon the open cwd handle; POSIX platforms would have deleted the tree.create-nexwire@latest .overwrote an existing project without warning —resolveTargetDirreturned early for.and skipped the "already exists" check applied to named targets, so running the command in a populated directory silently overwrote it..now requires an empty directory and refuses with a clear message, matching the behaviour of<project-name>.Docs still told users to copy
.env.exampleto.env— the scaffolder writes a working.env(SQLite, pre-migrated and pre-seeded) and now reports.env ready - no copy needed, butguide/quick-start.md(all four package-manager blocks) andguide/env.mdstill instructed a manualcp .env.example .envbeforeinstall. Besides being unnecessary, that command does not exist in Windows cmd or PowerShell, so the documented first step failed for Windows users — the same class of failure the scaffolder's own printed instruction had. The copy step is removed from the Setup blocks; a tip now states there is nothing to copy and, for anyone recreating the file by hand, gives the platform-correct command.guide/env.mdalso drops its barecpblock in favour of the same note.guide/database.mdquoted a CLI error that no longer exists — the page reproduced the "database already contains tables" message asUse 'npm run|pnpm|yarn|bun maker db:fresh --seed' to rebuild locally., which is wrong three ways:npm run|pnpm|yarn|bunis pseudo-syntax that appears nowhere in the code, the npm form is missing the--separator (so npm swallows--seed— the exact defect already fixed in the code), and the trailing "restore migration files" clause had been dropped. The quoted block now matches the string the CLI actually prints (framework/maker-cli/db/index.mjs), byte for byte.Scaffolded README advertised the wrong engine — the Features table said "Hono HTTP server", so a project scaffolded with
--engine=expressshipped a README claiming Hono. Now "Hono/Express HTTP server" across all six copies (rootREADME.md,template/{express,hono}/README.md,packages/create-nexwire/README.md,packages/create-nexwire/{express,hono}/README.md). The four scaffolded copies are byte-identical and the table column alignment is preserved.npm run maker db:migrate --seedsilently dropped the--seedflag -- npm consumes--seedas its own unknown CLI config instead of forwarding it to the script, so the command ran as a plaindb:migrateand no seeders executed. The npm warningUnknown cli config "--seed"was the only clue. npm requires a--separator:npm run maker db:migrate -- --seed(bun'sbun maker db:migrate --seedworks unchanged). Corrected in the four scaffoldedREADME.mdcopies (byte-identical), rootREADME.md,packages/create-nexwire/README.md,packages/docs/guide/database.mdandpackages/docs/cli/database.md. The maker CLI no longer prints the bun-only form: the--seedoption descriptions and the twodb:freshrecovery messages now show the npm form alongside the bun form.db:migrate --seedcrashed on any freshly generated module --module:makewrites a fully commented-out seeder stub (intentionally, so it compiles and inserts no fake rows), butseed.tsthrewSeeder file ... has no default export functionon any seeder without a default export. So the documented quick-start order -- generate a module, thendb:migrate --seed-- always aborted with exit code 1.seed.tsnow skips seeder files containing noexportstatements and reportsSkipped unfilled seeder <file> (no exports yet); a seeder that does export something but lacks a default function still throws, so genuine mistakes remain loud. Byte-identical across both engines (sha256 34a7f82b1ff2a0669d4332bc6e8a8fcdb210843cae1813f34eca30806c3a4dab).Redis Commander view left port 1369 dead when Redis was unavailable --
npm run maker dev -- --with-redis-view(orredis:view) withREDIS=falsein.envskipped launching the commander entirely, so the documented URLhttp://localhost:1369was a closed port and the browser only said "unable to connect". WithREDIS=truebut no Redis actually listening, the commander started and rendered its own raw connection error instead of the framework's diagnostic.redis:viewnow probesREDIS_URLwith a short TCP connect and, when nothing answers orREDISis disabled, serves a Redis Commander Unavailable page (naming the Redis URL) that identifies Redis Commander and the Redis key browser and points at/queuesfor BullMQ state -- deliberately not theframework/queue/ui.ts"Queue Dashboard Unavailable" page, which describes the bull-board route and would mislabel a key browser as a queue dashboard; otherwise it launches Redis Commander exactly as before. The dev-stack gate no longer suppresses the view when--with-redis-viewwas explicitly requested with Redis disabled, so the advertised port stays live. The CLI runtime (maker-cli/runtime/core.mjs) is plain.mjswith notsconfig allowJs, so the page markup is built locally rather than imported fromui.ts; the new functions are byte-identical across both engines (sha256 664f4a072755c0200aea2f72502b18af1077f4de4d57e9638bba7aedfd4e4e8e).Stale runtime log files shipped inside the npm tarball --
template/{express,hono}/src/storage/logs/app.logandfatal.log(up to 41 KB, 1118 lines) were untracked dev artifacts thatnpm packstill included, leaking local stack traces and absolute paths into every generated project. Present in publishedcreate-nexgen@3.2.1and carried into the 4.0.0 candidate. Deleting them was not enough: they are regenerated every time the app runs fromtemplate/honoortemplate/express(your own local run recreates them), and becausepackage.jsondeclares afiles: ["src","hono","express"]whitelist, a root.npmignoreis never consulted -- so npm packed them anyway.scripts/sync-template.mjs(the prepublish hook) now excludes*.logfrom the engine copies, which is the only place the rule is actually enforced. Verified by re-packing with the log files still present on disk: 490 -> 486 files, zero.login the payload..gitkeepstill ships, so the runtime log directory exists.First run could not survive a SQLite
DATABASE_URL-- the three core auth models (modules/auth/database/models/{role,user,notifications}.ts) were hardcoded todrizzle-orm/mysql-core(mysqlTable,int().autoincrement(),onUpdateNow()), so the documented "SQLite, MySQL, or PostgreSQL. Auto-detected fromDATABASE_URL" was false for a fresh project: pointingDATABASE_URLat SQLite madedb:migrategenerate zero tables (and report success), after which every query failed -- login and register were broken out of the box, and SQLite is exactly what a first-time developer is most likely to choose because it needs no server. The three model files are now nativedrizzle-orm/sqlite-core(integer("id").primaryKey({ autoIncrement: true }),text,integer({ mode: "timestamp" }),integer({ mode: "boolean" })); MySQL'sonUpdateNow()was dropped because SQLite has no equivalent, so the core schema is SQLite-only by default. In the same change the framework now ships the migration and its data:src/database/migrations/sqlite/(generated0000_init.sql+ meta) and a pre-migrated, pre-seededsrc/storage/database/nexwire.sqlite(rolesadmin/user, usersadmin@example.com/user1@example.com, bcrypt-hashedPassword@123) so a freshcreate-nexwireproject works with zero setup -- the scaffolder writes the.envfor you, thennpm run devand the seeded admin can log in immediately; the shipped migration makes subsequentdb:migratea no-op and future schema changes generate0001_*. Template.env/.env.exampledefault changed frommysql://...tosqlite:./src/storage/database/nexwire.sqlite. The three model files and the SQLite database file are byte-identical across express and hono (DB sha256 afce2f88755d40650bdcfbd6b55d2ca87df46561a6926c5eb40300245697c61b).Register / notify crashed with 500 on a SQLite database --
auth.controller.ts(register) andframework/notification/index.ts(notify) resolved the inserted row id frominsertResult.insertId, a mysql2-only field. libsql/sqlite awaits returnlastInsertRowidinstead, so on the new SQLite default every registration inserted the row (making the retry return 422 "Email already exists") then threwFailed to resolve inserted user idbefore issuing tokens. Both helpers now fall back through[0]?.insertId ?? insertId ?? lastInsertRowid, working on MySQL and SQLite. The models layer is byte-identical across engines, butauth.controller.tsis engine-native by design (Honoc.req.valid/c.jsonvs Expressreq.body/res.status().json()), so the fix was applied to the Hono handler and the Express handler separately; the Express handler's originalexpressidiom is preserved. Byte-identical across express and hono for the sharedframework/notification/index.ts(sha256 bb5e35c9b48f7caa506bd432fd5c39dbdaa488a5658744145eb44ae7943ca279).
[3.2.1] — 2026-09-24
Fixed
Scaffolder maker-cli runtime injects the dev UI origin into the spawned API --
create-nexgen's maker-cli runtime (express/honoframework/maker-cli/runtime/core.mjsandcore.mts, line 214) setsNEXGEN_FRONTEND_URL: "http://localhost:5173"on the API child process it spawns when the UI is enabled but the built SPA is absent (Vite dev server still running) -- this is the dev-without-build branch the URL resolver consumes. Byte-identical across both engines; shipped viacreate-nexgen@latest/nexgen@latest.useGumFormdropped the Axios response from theonSuccesscallback —form.post/put/patch/delete(submit()) calledonSuccesswith no argument, so the callback'sewasundefined(noe.response, noe.data). Thevisit()path passedresponse!correctly; nowsubmit()does too —onSuccessin both paths receives the full AxiosResponse (e.data/e.status/e.headers). Arrays (onStart,onSuccess) no longer trigger the "Expected 0 arguments, but got 1" TS error.FormSubmitOptionsnow mirrorsGumVisitOptionswith the full hook surface —onBefore,onStart,onProgress(event),onSuccess(response),onError,onFinish. Fixed a strayawaitin the non-asynconUploadProgresshandler (onProgress?.(event)is called synchronously, matchingvisit()). Byte-identical across express and hono engines.Dev reset/verify links pointed at the API origin instead of the Vite dev server —
urls.url()in dev with UI enabled but no UI build yet (Vite dev server still running) resolved toAPP_URL(port 3000) instead of the SPA dev origin (port 5173). The resolver now consumes theNEXGEN_FRONTEND_URLthat maker-cli injects when the built SPA is absent, so reset/verify email links point at the Vite dev server (e.g.,http://localhost:5173/reset-password) in dev-without-build, and at the app origin (single port, e.g.,http://localhost:3000/reset-password) when the framework serves the built SPA — matching browser behavior in production and dev-with-build. Byte-identical across express and hono engines.forgetPassword.vueonSuccessnow consumes the submitted response — the callback takes the AxiosResponse (res) and displaysres.data.messagefrom the API instead of a hardcoded "reset link has been sent" text, so the UI reflects the server's actual reply. Byte-identical across express and hono engines.resetPassword.vueonSuccessnow consumes the submitted response — the callback takes the AxiosResponse (res) and displaysres.data.messagefrom the API before redirecting to login. Byte-identical across express and hono engines./api-docsserved no favicon on the API origin — the Scalar docs page ships without a<link rel="icon">, so browsers requested/favicon.icofrom the API server. The express engine had no/favicon.icoroute (404, empty tab icon) and hono returned Stoker's rocket-emoji SVG instead of the real icon. Both engines now servesrc/resources/src/assets/images/favicon/favicon.icoat/favicon.ico(image/x-icon), matching the SPA favicon; the route is configured fromsrc/config/openapi.ts(scalar.favicon) and served byconfigureOpenApionOPEN_API=true, soapp.tsstays untouched; hono'sserveEmojiFaviconmiddleware was removed. Byte-identical across express and hono engines.src/config/security.tsexported noSecurityConfigtype —config/index.tsimportedtype SecurityConfigfrom./security.js, but the template file never exported it (TS2724 on the scaffolded app). Addedexport type SecurityConfig = typeof securityConfig;and fixed a straydata: data: blob:duplicated source in the dev CSPimg-srcon the express template. Both templates were also re-synced intopackages/create-nexgen(which still had the pre-CSP skeleton), so all four copies are byte-identical. Byte-identical across express and hono engines.Guide intro claimed cache "falls back to the database" when Redis is down — it does not: the cache is Redis-only. When Redis is unavailable,
cache.getreturnsnull,put/forgetbecome no-ops, andrememberrecomputes the fresh value on every call. Correctedguide/introduction.mdto say caching disables itself gracefully instead of suggesting a database cache layer.Guide intro auth examples used the wrong token expiries — the
jwt.generateTokensnippets passed3600for access and604800(7 days) for refresh, contradicting the actual config (900= 15 min access,3600= 1 hour refresh,2592000= 30 days "remember me"). Fixed the examples to use the defaults and show the remember-me tier. The expiry logic inauth.helpers.ts/jwt.ts/cookie.tswas verified correct and engine-consistent.Guide intro "Self Deploy to VPS" showed only npm commands — converted both deploy command blocks (init/remote and promote/import) into
::: code-grouptabs for npm, pnpm, yarn, and bun, matching the other guides.--file=flag syntax per package manager (npm run makeruses-- --file=…; directmakercalls use--file=…).
Changed
- Node floor lowered from
>=24to>=22.12.0— the manifests (create-nexwire, both engine templates, and the two mirrored copies) declared"node": ">=24", which made the scaffolder look like it demanded a bleeding-edge runtime and locked out the still-supported Node 22 line. Node 24 ("Krypton") has been Active LTS since 2025-10-28, but the floor was never a technical requirement: nothing in the framework uses a Node 24-only API — TypeScript is executed through thetsxpackage (not native type-stripping), the onlyimport.meta.dirnameuse needs 20.11+, andglobis the npm package rather thanfs/promises.glob. The real floor is imposed by the frontend build: Vite 8.3.1 declaresnode: ^20.19.0 || >=22.12.0, so>=22.12.0is the lowest honest bound. That keeps both security-supported LTS lines working — Node 22 (Jod, Maintenance LTS to 2027-04-30) and Node 24 (Krypton, Active LTS to 2028-04-30) — and deliberately excludes Node 20, which reached end-of-life on 2026-04-30. Docs updated in lockstep: the "Requires Node.js >= 22.12" line and the Runtime table in all four engine README copies plus the two showcase READMEs, and the requirement bullet inpackages/docs/guide/quick-start.md. The two lockfiles' rootenginesentries were updated to match; the>=24.0.0strings remaining in them belong to@types/node's own peer range and are third-party, not ours. Engine README copies remain byte-identical across express and hono (4 copies, sha256 d8e1600a4e74). homepagenow points at the GitHub Pages docs instead of a dead domain —create-nexwiredeclared"homepage": "https://nexwire.dev", a domain the maintainer does not own. npm rendershomepageas the Website link in the package page sidebar, so it shipped a link that goes nowhere (the same defect is visible on the already-publishedcreate-nexgen@3.2.1, whosehomepageishttps://nexgen.dev; a published version's manifest is immutable and 3.2.1 is past npm's 72-hour unpublish window, so it can only be steered withnpm deprecate, never corrected).homepageis repointed tohttps://niyamulahsan.github.io/nexwire— the VitePress docs inpackages/docs, deployed to GitHub Pages by.github/workflows/deploy-docs.ymlon every push tomainthat touchespackages/docs/**. Note the URL only resolves once thenexwirerepository is public and that workflow has run onmain; before that the path 404s. The two showcase READMEs (rootREADME.mdandpackages/create-nexwire/README.md, the GitHub landing page and the npm package page) carried the same dead domain in two more places: the centred logo<a href="https://nexwire.dev">now links to the GitHub repo, and the docs badge label / footer text no longer advertisenexwire.devas if it were a live site. The scaffolded project READMEs undertemplate/{hono,express}are intentionally left at their previous wording, since they are a per-project starting point rather than a showcase surface; they remain byte-identical across express and hono (4 copies, same sha256).- The two showcase READMEs now open with the value proposition, not an ingredient list — the previous opening read "a batteries-included TypeScript framework that combines modular backends (Hono, Express) with Drizzle ORM, BullMQ, Socket.IO, Redis, and a Vue 3 SPA frontend", which restated the dependency list and answered nothing about what it saves you; a reader's takeaway was "Hono + Drizzle + BullMQ + Socket.IO + Vue, which I can install myself". The tagline is now a claim ("Stop wiring the stack together. Ship the app."), followed by the problem (the same ten capabilities are a week of glue code whose failures are silent — a job that never runs, a socket that never joins a room), then five concrete differentiators: runnable the second it is created (pre-migrated, pre-seeded SQLite, credentials in the text), nothing to register (
maker module:make blogplus auto-discovery), one import per subsystem (@/framework/facade.js, Redis-free in-memory fallback), optional infrastructure (Redis/mail/S3 circuit breakers with real fallbacks), and one-command VPS deploy over SSH. Quick Start was made consistent with the claim it now makes:npm run maker db:migrate -- --seedis dropped from both the Hono and Express blocks (the shipped SQLite database is already migrated and seeded, and both seeders are idempotent — theyfindFirstbefore inserting) and replaced by a note saying the step is only needed after pointingDATABASE_URLat MySQL/PostgreSQL. TheFeaturestable's database row was corrected at the same time: it claimed "SQLite, MySQL, or PostgreSQL. Auto-detected fromDATABASE_URL", which no longer describes the shipped core tables (see the SQLite bullet above — the auth/notifications models are nowsqlite-core); it now reads "SQLite by default, shipped pre-migrated and seeded; MySQL/PostgreSQL also supported, dialect auto-detected". Scope is deliberately limited to the GitHub landing page and the npm package page; the scaffolded READMEs are untouched and stay byte-identical across express and hono. - URL origin resolution is now three-knob and byte-identical across engines — the origin decision in
framework/support/url.ts(both engines) is:frontendUrl(explicit SPA origin — wins) →uiEnabled+ built SPA (hasUiBuild()) (framework serves it — same/APP_URLorigin) →uiEnabled+ no build (dev Vite dev server still running —process.env.NEXGEN_FRONTEND_URL, injected by maker-cli, falls back toAPP_URL) → API-only (APP_URL). Comment docs updated to document all three branches.
[3.2.0] — 2026-09-22
Added
expressengine —create-nexgencan now scaffold a project on Express instead of only Hono. Pick the engine at scaffold time with--engine=express(Hono stays the default; pass--engine=honoexplicitly).
Changed
- Both engines ship the same facade surface — the identical set of app-facing verbs (
storage,cache,db/database+paginate*,command/dispatchCommand,dispatchEvent,shouldQueue/queue,defineSchedule,session,notify,broadcast, plus support helpers), each implemented natively in its own engine's idiom. Hono's facade relies on@hono/zod-openapi(createRoute,group,createRouter); Express's uses its native router conventions. The files are not byte-identical by design — what's identical is the surface (same verbs, same signatures). Switching engines requires no module, facade, or route changes (the facade surface is the contract, not the implementation bytes). - Orchestration endpoints identical —
/ready,/live,/metrics, and/healthare auto-wired by both engines' HTTP layer (http/app.ts) with byte-identical K8s probe semantics. - App settings identical — security, validation, rate limiting, circuit breaker, and metrics counters are config-driven app settings in both engines, wired by the framework at boot, and never part of the facade.
Security
- Security disclosure added to both READMEs — a
## Security Vulnerabilitiessection asks researchers to email the maintainer directly rather than opening a public issue, matching the common coordinated-disclosure convention.
[3.1.0] — 2026-09-09
Added
- API docs split into per-function pages — the deskapp-style API reference now has one page for every facade export (32 functions) with a Function | Purpose | Guide overview table, replacing the bundled pages (
http,events,queue,paginate,support,realtime→broadcast,scheduler→defineSchedule). - Documentation site header — deskapp-style top nav (
Home,Quick Start,Guide,API,CLI,Deploy) with inline SVG icons on every nav and sidebar entry.
Changed
FRONTENDenv renamed toUI—frontendEnabledconfig key renamed touiEnabled(template/src/config/app.ts). No change in behavior; the oldFRONTENDvalue is no longer read.- Documentation site header — deskapp-style top nav with inline SVG icons on every nav and sidebar entry.
- maker-cli restructured — flattened
src/levels/layout intodb/,deploy/,module/,runtime/,utils/directories withindex.mjsat the root. Command surface unchanged (52 commands).
Fixed
db:module:seed <module>— seeds now target the given module argument (secondArg || firstArg) instead of the first argument.deploy:workflow:remote --dry-run— dry-run no longer proceeds with the remote workflow half-run.deploy:db:import:remote --dry-run— removed dry-run guard so remote DB import correctly skips execution.--force/--dry-runon module make commands — flag parsing refactored across all 7 module make commands (rawArgs.slice(name ? 3 : 2)).SOCKETenv in deploy generation — deploy env templates fall back to"false"whenSOCKETis not set.
Documentation
- sarra removed from API docs — project-specific examples replaced with generic framework guidance.
[3.0.8] — 2026-08-21
Added
broadcastexported from facade —broadcast()can now be imported directly from@/framework/facade.jsfor direct Socket.IO emission withoutdispatchEvent().- OpenAPI config file (
src/config/openapi.ts) — new editable config for Scalar theme/layout/docsPath/specUrl/title/version/description.
Changed
openapi.tsreads from config — Scalar and OpenAPI settings now come fromsrc/config/openapi.tsinstead of being hardcoded.config/index.ts—openApiConfigimport/export added;config.openAggregatedincludes OpenAPI settings.
Documentation
- 24 discrepancies fixed across 16 docs files — wrong imports, wrong defaults, fake env vars, missing options, syntax errors.
- Realtime docs — added
broadcast()direct usage alongsidedispatchEvent(). - Scheduler docs — added
queue,job,data,immediatelyoptions. - New:
guide/openapi.md— full OpenAPI setup page (Scalar, metadata, auth, custom theme). - API reference "Real world" sections — authentic examples from the sarra app (auth router, event/queue flows, pagination, storage exports, cookie/jwt/password) added across the API pages.
[3.0.7] — 2026-08-21
Fixed
--withflag parsing —parseWithOptionsnow splits on/[,\s]+/so commas and spaces both work, fixing yarn's--with=redis,maildev,studiobeing treated as a single argument.@hono/zod-openapipinned to~1.5.3—^1.5.2resolved to 1.6.1 which pulls@asteasolutions/zod-to-openapi@^9, breakingstoker's peer dependency on^8. Tilde keeps it on 1.5.x.redis-commanderbumped to^0.9.0— old^0.4.5usesexpress@3+ejs@0.8which crash with pnpm's newer EJS resolution.0.9.0usesexpress@4+ejs@3.
Documentation
- Quick Start — Package Manager Setup — new section explaining extra files needed for pnpm (
pnpm-workspace.yamlwithallowBuilds) and yarn (.yarnrc.ymlwithnodeLinker: node-modules+dependenciesMetainpackage.json).
[3.0.3] — 2026-08-20
Fixed
- create-nexgen yarn PnP crash — scaffolder always downloads the tarball from the npm registry instead of using a local copy, fixing
cpSyncENOENT errors under Yarn PnP (zip-mountednode_modules). - create-nexgen leaked
index.js— tar extraction now only includespackage/template/*paths, preventing the CLI source file from appearing in the scaffolded project. - create-nexgen peerDependencies synced — replaced Winston with Pino, bumped
@bull-board/*to ^8,nodemailerto ^9,redis-commanderto ^0.4, added@libsql/client,@socket.io/bun-engine,bullmq-durable,croner,postgres; engines updated tonode >=24,bun >=1.3.0,pnpm >=10,yarn >=4. - create-nexgen template dependencies synced — root
template/package.jsonupdated to match all framework dependencies.
[3.0.0] — 2026-08-19
Overview
Unified environment and configuration handling across backend and frontend, added runtime feature toggles (OPEN_API, REDIS, FRONTEND, SOCKET) to the Zod schema, consolidated deploy env templates, switched logger from Winston to Pino, added keyboard shortcuts to Dialog, rewrote the entire documentation site with VitePress, and professionalized the GitHub/npm READMEs.
Features
Environment & Config
REDIS,OPEN_API,FRONTEND,SOCKETin Zod schema — all four feature toggles now live inenv.tswith boolean transforms (!== "false" && !== "0"), making them the single source of truth. Config files (app.ts,redis.ts,realtime.ts) read from the schema instead of hardcoding.SOCKETenv toggle —config/realtime.tsreadsenv.SOCKETinstead of hardcodingenabled: true. The ViteFrontend config also readsprocess.env.SOCKETto control the Pulse client build.queueUiAllowedEmailsmoved — moved fromredis.tstoqueue.tsasallowedEmails: ""since it's queue-specific.
Deploy
- Deploy env templates rewritten — all three stubs (
mysql.server.stub,postgres.server.stub,sqlite.stub) now includeOPEN_API,SOCKET,FRONTEND,REDIS,REDIS_PREFIX,FRONTEND_URL,JWT_*,COOKIE_SECRET,STORAGE_*,MAIL_*with proper comments distinguishing config-file settings from env-var secrets. - Deploy core.mjs —
OPEN_APIandSOCKETvalues now read from the root.envand injected into generated templates. deploy:initauto-creates.env— workflow copies.env.example→.envautomatically; users only edit.examplefiles.deploy:workflowenv sync —ensureEnvFile()ensures bothdeploy/.envanddeploy/server/.envexist before starting containers.
CLI
module:make-testadded — new command for generating test scaffolding for modules.db:freshalias —db:migrate:freshnow works as an alias.db:resetaliases —db:migrate:resetanddb:wipenow work as aliases.serve --srcflag — allows specifying a custom source directory.queue:workcomma-separated queues —--queue=default,mail,maintenancenow properly parses comma-separated values.deploy:init --pmsupportsbun— package manager flag now acceptsbunin addition tonpm|pnpm|yarn.
Frontend
- Dialog keyboard shortcuts —
bindKeyswith Enter (OK) and Escape (Cancel), guarded byshell.isConnected.stopPropagationadded to prevent event bubbling. - Auth pages use logo — all five auth pages (
login,register,forgotPassword,resetPassword,verifyEmail) displaylogo.pngfrom the Sidebar component. - Pulse timing fix —
pulse.channel()moved toonMountedwith null guard to prevent race conditions. - Theme dark primary — template dark theme primary changed from
$warning(yellow) to#3b8eed(VitePress blue).
Logger
- Winston → Pino —
support/logger.tsrewritten to use Pino withfatal,trace, andchild()methods.config/logging.tsnow uses Pino-style config (level: "info",httpRequests: true).
Fixes
- Boolean parsing unified — all maker-cli scripts use
!== "false" && !== "0"for boolean env parsing, matching the Zod schema logic. - Plain/OpenAPI stubs — example and notification modules now have plain and OpenAPI variants based on
OPEN_APIflag. - Sync-template basename check —
skipDirsfilter insync-template.mjsnow correctly checks basename instead of full path. - Notification module refactor — removed 4 injection functions and Vue file generation; backend-only.
- DataTable sort removed — documentation corrected to reflect that DataTable does not handle sort internally.
useGumFormonProgressremoved — documentation corrected;form.progressis updated internally, not via options.useAuthcomposable — documentation rewritten to reflect actual API:user,isAuthenticated,setUser,clearUser,hasRole().
Documentation
- Full VitePress redesign — docs site rebuilt with VitePress, blue brand color (
#3b8eed), identical layout structure to Vue.js docs. - Sidebar restructured — new sections: Getting Started, Essentials, Services, Frontend, Support, Others.
- Logo & favicon — integrated into docs
public/, template assets, Sidebar.vue, and all auth pages. - Deploy docs rewritten —
overview.md,local.md,remote.mdrewritten to match actual source code: corrected Dockerfile examples, file trees, env variable tables, workflow step order, and addedrsyncSshPath/rsyncSshOptionsconfig fields. - CLI docs fixed — command counts corrected (Module 16, Database 14, Runtime 14, Deploy 6), missing
module:make-testadded,--viewoptions corrected (nobullmq),serve --srcdocumented. - Components docs rewritten — DataTable (props/slots/events/PaginatedData interface), Datepicker (4 modes), Select (FetchPack/cascade/resetKey), FeatureButton.
- Composables docs rewritten —
useAuth()actual API,hasRole(),authUser. - Helpers docs rewritten —
formatCompactNumber,inArray,empty,downloadFile,downloadExcel,formatTaxPeriod,formatDate. - Support docs fixed — cookie name (
nexgen), JWT expiries (900/3600/2592000), logger (Pino not Winston), mail settings moved to config file section. - Env docs updated —
SOCKETadded to Application table,FRONTENDdescription updated, config entries removed. - Modules docs — default column swapped to
OPEN_API=false. - READMEs rewritten — GitHub README with donate button (Support Kori + GitHub Sponsors), runtime/package manager tables; npm README with professional text.
Dependencies
- Logger: Winston replaced by Pino
- Docs: VitePress added as dev dependency
Upgrade Notes
This is a major release with breaking changes. Upgrading from v2.x requires manual steps.
- Folder structure changed — review the new architecture and adjust imports if you moved files outside the standard structure.
- Update
config/realtime.ts— now readsenv.SOCKETinstead of hardcodingenabled: true. EnsureSOCKETis set in your.env. - Update
config/queue.ts—queueUiAllowedEmailsmoved fromredis.ts. If you customized it, move the value. - Update
.env— addOPEN_API=false,REDIS=false,FRONTEND=true,SOCKET=trueif not present. - Re-run
deploy:init --force— deploy env templates changed significantly; regenerate all deploy files. - Logger — Winston replaced by Pino. If you imported
winstondirectly, switch topino. Runnpm installto pick up the new dependency. - Notification module — Vue file generation removed. If you relied on
NotificationBell.vueorindex.vuestubs, they no longer exist. - Boolean env parsing — unified to
!== "false" && !== "0". Values like"no"or"off"are now treated astrue. Use"false"or"0"to disable. - CLI command changes —
deploy:localanddeploy:remoteremoved; usedeploy:workflowanddeploy:workflow:remote.--view=bullmqremoved fromdevcommand.
[2.3.0] — 2026-08-12
Overview
Consolidated the 16 deploy CLI commands into a single 6-command surface (deploy:init, deploy:workflow, deploy:workflow:remote, deploy:workflow:promote, deploy:db:import, deploy:db:import:remote) and added nginx-proxy reverse proxy support to the generated compose files. Also shipped a batch of bug fixes across the scheduler, mail, database import, and frontend components, plus a new Datepicker component and a Public Sans UI font.
Features
Deploy CLI
- Deploy commands consolidated from 16 → 6 — the many
deploy:create:*,deploy:server,deploy:app,deploy:workflow:*variants collapsed into:deploy:init— generate app + server scaffolding and both workflow configs (--app-only,--server-only,--dev,--force,--runtime,--pm)deploy:workflow— local pipeline from a config file or flags (--server-only,--app-only,--refresh,--dry-run)deploy:workflow:remote— upload viarsync/scpand deploy on a remote hostdeploy:workflow:promote— run local workflow, then remote workflowdeploy:db:import/deploy:db:import:remote— SQL dump import with auto-detected MySQL or PostgreSQL- Legacy names (
deploy:create,deploy:server,deploy:app,deploy:db:import) still work inside workflow config files
deploy:initgenerates workflow configs —deploy/workflow.local.jsonanddeploy/workflow.remote.jsonare created automatically, so no separate init step is needed. The local workflow usesdeploy:workflow --server-only/--app-onlysteps.- Reverse proxy (nginx-proxy) support — generated app compose attaches to the external
nginx-proxynetwork and readsVIRTUAL_HOST,VIRTUAL_PORT,LETSENCRYPT_HOST,LETSENCRYPT_EMAILfromdeploy/.envfor domain routing and auto-SSL. APP_URLenv variable — added to deploy env templates (used for links, redirects, CORS). Also wired into the app composeAPP_URLdefault.- PostgreSQL dump import —
deploy:db:importnow auto-detects the dialect (MySQL or PostgreSQL) and restores PostgreSQL into a fresh database by default (DROP DATABASE ... WITH (FORCE)+ recreate); pass--no-dropto keep the existing database. preDeployCommands— remote workflow config gained an array of commands run on the remote host before the app starts (e.g.docker rm -f old-app).- pgAdmin servers.json mount — server compose mounts
./pgadmin/servers.jsoninto the pgAdmin container as read-only. - Bun Dockerfile copies
.env—deploy/.envis copied into the image before schema generation sodb:schemaruns with the rightDATABASE_URL.
Mail
MAIL_ENCRYPTIONenv variable — newssl|tls|noneoption (defaultnone).sslenables implicit TLS (port 465),tlsenables STARTTLS (port 587). The mail transport now setssecurefrom this value instead of hardcodingfalse.
Scheduler
- Run-on-init + console output —
runOnInitschedules now run sequentially after all cron jobs are registered, under a "Running Scheduled Commands" header. Every run logs the task name with its elapsed time (orFAILon error).startScheduler()returns the number of registered schedules (logged asScheduler started [N schedule(s)]).
Frontend
Datepicker.vuecomponent — new date/datetime/month/year picker wrapping@vuepic/vue-datepicker(added as a dependency), with a nativeinput[type=month]fallback for month mode on Firefox.- Public Sans font — self-hosted WOFF2 (latin + latin-ext) wired into
body,--bs-body-font-family, and--bs-font-sans-serif. - Skeleton theme variables —
--app-skeleton/--app-skeleton-hiadded to both light and dark themes;DataTableSkeletonuses the theme-awareskeleton card borderclasses instead of hard-coded white. FeatureButtonrender slot — supports a default slot for compound header controls (dropdowns etc.); header renders the slot inside a wrapper with the same order/class as a normal button.attrsprop renamed tobuttonClass.Inputechallan-numbercategory — new formatting category that uppercases and forcesV+ 9 digits.Selectsearch pinning — after clearing a search, the last search results are pinned on top of the refetched full list so you don't lose your place.TextAreadefault rows — addedrows="3"default.Header.vue— "My Profile" is now a real<router-link to="/profile">instead of a deadhref="#"; ordering classes andtitleattributes cleaned up.Pagebar— simplified title logic (titleshown only when there's no default slot).Pagination— adds apagination-${variant}class.- Dialog theming — overlay uses
var(--app-backdrop)/var(--app-text)and the card header/footer no longer hard-codebg-white, so dialogs follow light/dark mode automatically. - Helpers — added
formatTaxPeriod()(yyyy-MM→MMM yyyy) andformatDate()(ISO →dd MMM yyyy). .fs-7— 12px font-size helper class.
Fixes
- Deploy DB dialect detection —
detectMigrationDialectFromSqlnow checks PostgreSQL hints first (GENERATED ALWAYS AS IDENTITY,bigserial,serial,timestamp with time zone,public.), uses MySQL-only hints (auto_increment,engine=, backticks), and no longer misdetectsvarchar(as MySQL. - Migration hooks under Bun —
runMigrationHooks()runsbundirectly when running under Bun instead oftsx dist/cli.mjs, fixingdb:migrateunder the Bun runtime. auto-migrate.shhardening — normalizesAUTO_MIGRATE(trims, lowercases, strips Windows\r) and documents the flow;set -euretained.- Queue worker queues —
maker devworker now also processes themaintenancequeue (--queue=default,mail,maintenance). Button.vue— removed leftoverd-flex justify-content-center align-items-center custom-btnclasses.- Scheduler error handling — a failing schedule run logs
FAILinstead of crashing the scheduler process. RATE_LIMIT_LOGIN_MAXdefault — raised from10to60so legitimate login attempts aren't rate-limited during password reset workflows.
Documentation
- Deploy guide rewritten —
cli/deploy.md,deploy/overview.md,deploy/local.md,deploy/remote.mdupdated for the 6-command surface, including a removed-commands migration table and the new remote config (upload,preDeployCommands) and nginx-proxy SSL section. - Environment guide — documented
MAIL_ENCRYPTIONand the newRATE_LIMIT_LOGIN_MAXdefault (also updated in the Rate Limiter guide). - Mail guide — documented the
ssl/tls/noneencryption modes. - Scheduler guide — documented the new console output and run-on-init behavior.
- Components guide — added
DatepickerandFeatureButtonsections, Input formatting categories, Select search pinning, and updated the component list. - Assets guide — documented the Public Sans font and the theme CSS variables.
- Helpers guide — documented
formatTaxPeriod()andformatDate(). - Runtime guide — updated the
maker devworker queues.
Dependencies
- Added
@vuepic/vue-datepicker@^14(root + template)
[2.2.5] — 2026-06-27
Changed
- Modal.vue refactored — removed Bootstrap
ModalJS dependency. The component now uses a nativedata-bs-dismissbutton reference (closeButton) for closing, eliminating the need forimport Modal from "bootstrap/js/dist/modal.js", manual instance management, and backdrop cleanup logic. Theopen()method was removed as the parent caller manages visibility via the Bootstrap data API. Template attributes were also collapsed to single lines.
Removed
- Bootstrap Modal JS import — removed
modal.jsimport andModalinstance lifecycle (getOrCreateModal,onMounted,onBeforeUnmount,dispose). The modal is now fully Bootstrap data-API-driven.
[2.2.4] — 2026-06-22
Changed
- Auto-create
public/at boot —createKernel()now callsensurePublicDir()during startup (whenFRONTEND=true), creating thepublic/directory if missing. The frontend static middlewares (frontendStaticMiddleware,frontendIndexMiddleware) are also lazily initialized via wrapper functions.
Removed
create-nexgentemplate directory — deleted all files underpackages/create-nexgen/template/(.dockerignore,.env,.env.example,README.md,biome.json,drizzle.config.ts,gitignore-stub,package.json, database migrations,src/env.ts, framework internals, frontend resources, router, stores, plugins, types, and config files). These files are no longer bundled with thecreate-nexgenpackage.- Bumped
create-nexgenversion —packages/create-nexgen/package.jsonupdated from2.2.3to2.2.4.
[2.2.3] — 2026-06-19
Changed
- Consolidated type declarations — moved all
declare moduleblocks from scattered.d.tsfiles (src/resources/src/env.d.ts,src/resources/src/types/luxon.d.ts,src/framework/database/optional-db-drivers.d.ts) into a singlesrc/types/global.d.ts. Updated both root and resourcestsconfig.jsonto include the central file. This gives both the backend and frontend projects access to module declarations (luxon, nodemailer, pg, vue-select, bootstrap JS,*.vue, better-sqlite3) without duplication.
[2.2.2] — 2026-06-19
Fixed
- Queue worker database initialization —
worker.tsnow callsinitDatabase()before starting the queue worker andcloseDatabase()on shutdown. Previously, job handlers that used the database threw "Database is not initialized" when running in a separate worker process.
Changed
- BullBoard dynamic queue discovery —
ui.tsnow scans Redis for all queue:metakeys at boot and registers them automatically, eliminating the need to hardcode queue names inensureQueues(). In development mode, a 15s poll timer rescans Redis and dynamically adds newly discovered queues to BullBoard viaaddQueue(). Polling is skipped in production where all queues are known upfront. setupBullBoard()is now async —kernel.tsusesawait setupBullBoard()to support the Redis scan on boot.- Graceful shutdown —
server.tsimports and callsstopBullBoardPoll()to clear the dev poll timer on shutdown.
[2.2.1] — 2026-06-17
Fixed
- Biome import fixup —
organizeImportsstripped component imports used only in Vue<template>blocks. Restored 150+ missing imports across stats project, nexgen template, and create-nexgen template. Also reverted unsafenoUnusedVariablesrenames (_logout,_themeIconClass,_startYear/_currentYear) that broke template bindings. Added missingvSelectimport (vue-select) and fixedObject.hasOwn→inoperator for ES2020 compat.
[2.2.0] — 2026-06-17
Overview
Replaced ESLint + Prettier with Biome, unifying linting and formatting under a single tool with consistent code style applied across the entire codebase. Added paginateModel() for relational query pagination, skipFetch option in Gum for client-side navigation, and FeatureButton for extensible nav bar buttons. Fixed paginate count query and removed dead Refresh.vue component.
Features
Tooling
- ESLint + Prettier removed — replaced with Biome v2.5.0 (
preset: "recommended") - Biome config — 8 rule overrides matching previous ESLint behavior:
noUnusedVariables,useExhaustiveDependencies,noConsole,noSwitchDeclarations,useOptionalChain,useDefaultSwitchClause,noParamAssign,useVueMultiWordComponentNames - package.json scripts —
lint,lint:fix,format,format:checknow use Biome - Removed 8 devDependencies —
eslint,@eslint/js,eslint-plugin-vue,typescript-eslint,prettier,eslint-config-prettier,@typescript-eslint/parser,@typescript-eslint/eslint-plugin - Deleted config files —
eslint.config.js,.prettierrc,.prettierignore .gitignore— addedcoverage/andsrc/storage/trash/
Database
paginateModel()— new paginator for Drizzle relational queries (db.query.table.findMany) with support forwhere,with,columns,extras,orderBy, and customtotal/datacallbacksresolvePath()— extracted URL resolution helper used bypaginate()andpaginateModel()- Fixed paginate count query — replaced broken
.as("paginate_rows")with proper inner select for accurate total row counting
Frontend
FeatureButton.vue— new component for registering extensible nav bar buttons viainject("featureButtons")- Header.vue — replaced commented-out refresh button with
featureButtonsslot; removed deadRefresh.vue skipFetchoption —gum.visit()/useGum()now supportsskipFetch: truefor client-side route transitions without a server roundtrip
Documentation
- Rate Limiter — new guide page covering per-IP and per-session rate limiter configuration
- Database — updated migration and seeding docs
- Environment — documented new
RATE_LIMIT_WINDOW_MS,RATE_LIMIT_MAX,RATE_LIMIT_LOGIN_MAXvariables - Events & Queue — minor corrections
- Components — expanded API reference
- Gum — documented
skipFetchoption
Fixes
- Paginate count query — count subquery now uses
db.select({ val: sql\1` }).from(query.as("_inner"))instead of broken.as("paginate_rows")` pattern - Subcriteria dropdown —
subcriteriaDDreturnsnullwhen no criteria is selected, preventing unnecessary API requests - FloatButton — simplified template; renamed unused
floatStyleto_floatStylefor Biome compliance - env.d.ts — added blank line separators between
declare moduleblocks for Biome formatting
Dependencies
- Runtime: Node.js >= 24 or Bun >= 1.3 (unchanged)
- Database: SQLite (default), MySQL, or PostgreSQL (unchanged)
- Optional: Redis (unchanged)
- Dev: ESLint + Prettier removed, Biome v2.5.0 added
Upgrade Notes
Run npm install to pick up new devDependencies. Run npx biome check --write . to reformat any open feature branches. The paginate() function's internal count query changed — if you were relying on the old .as("paginate_rows") pattern, no migration needed as it was internal.
[2.1.1] — 2026-06-05
Overview
First stable release of nexgen — a full-stack TypeScript framework built on Hono, Vue 3, and Drizzle ORM. This release covers the complete feature set: modular architecture, Redis-backed services, real-time broadcasting, authentication, OpenAPI docs, frontend SPA, maker CLI, and Docker deployment.
Features
Core Framework
- Hono HTTP server with Zod-validated middleware stack (session, CORS, logger, rate limiter)
- Modular monolith — self-contained modules under
src/modules/with auto-discovered routes, jobs, models, and seeders - OpenAPI / Scalar — auto-generated API documentation at
/api-docswith JSON spec at/doc - Health endpoint at
GET /health - File storage — local disk or S3-compatible storage with signed URLs
- Dual database — SQLite, MySQL, or PostgreSQL via Drizzle ORM
Authentication & Security
- JWT auth — access + refresh token rotation with signed httpOnly cookies
- Email verification — optional
AUTH_REQUIRE_EMAIL_VERIFICATIONflow - Password reset — token-based forgot/reset with queue-emailed links
- Role middleware —
requireRole("admin")guard for admin routes - Rate limiting — per-IP rate limiter middleware
Redis-Backed Services (optional)
- Cache — TTL-based key-value cache with
cache.get/put/forget/remember - Session — server-side session store with httpOnly cookie, auto-refresh
- Queue — BullMQ job processing with
shouldQueuedecorator, retries, backoff - Realtime — Socket.IO with Redis adapter for multi-instance broadcasting
- Scheduler — cron-based
defineSchedulewith distributed Redis lock - BullBoard — live queue dashboard at
/bullmq - Redis Commander — GUI at configurable port (1369)
Events & Realtime
- String-based event dispatcher —
dispatchEvent(name, payload, options)with broadcast and queue support - Socket.IO server — automatic room joining by auth, user ID, and role
- Frontend Pulse plugin —
pulse.channel(name).listen(event, callback)API - Notifications system —
notify()persists to database and optionally broadcasts + emails - Mail queue — dedicated
mailqueue for async email delivery (nodemailer + SMTP)
Maker CLI
maker dev— starts API server, frontend HMR, queue worker togethermaker module:make— scaffolds controllers, routes, models, seeders, jobs, schedulesmaker db:migrate --seed— generates schema, runs migrations, seedsmaker serve --prod— production mode with compileddist/maker queue:work— BullMQ worker processmaker schedule:work— cron scheduler workermaker deploy— Docker-based deployment with nginx-proxy and SSL
Frontend (Vue 3 SPA)
- Gum plugin — Inertia-style page visits, form handling with validation errors, scroll preservation,
useGumRemember()for local UI state - Pulse plugin — Socket.IO realtime channels with room-based event listening
- Dialog plugin — programmatic
alert(),confirm(),prompt()modals - Auth pages — login, register, forgot/reset password, email verification
- UI components — DataTable (server-side pagination), Select (API-fetched with infinite scroll), Modal, Toast, Button, Input, Switch, and 12 more
- DataTable — full server-side pagination, search, bulk delete, skeleton loading, slot-based customization
- Theme system — light/dark/auto with SCSS variable overrides
- Route progress bar — animated navigation indicator
- Pinia stores — auth store with session bootstrap, admin-ui store with sidebar + theme state
- Axios interceptor — 401 auto-redirect to
/login
Documentation
- Comprehensive VitePress docs with full API reference for every plugin, component, and subsystem
- Dedicated pages for Gum, Pulse, Dialog, Router, Stores, Composables, Components, Validation, Assets, Axios
- Architecture guide with boot sequence diagram and middleware stack
- Complete env variable reference with feature interaction map
- Module development guide with OpenAPI stub variants
Deployment
- Docker Compose — two-layer deploy: app server + nginx-proxy with auto-SSL
- Local deploy —
maker deploy:localwith containerized MySQL and Redis - Remote deploy —
maker deploy:remotewith SSH and Docker context - CI/CD — GitHub Actions workflow for docs deployment
Fixes
- Signed cookie auth with separate frontend/app URL support
- Raw SQL query support in migrations (indexes, alter table)
- Seeder execution sequence ordering
- OpenAPI route tag placeholder handling
- Modal and toast component edge cases
- Zod async
safeParsefor async validation schemas - Template sync for publish workflow
Dependencies
- Runtime: Node.js >= 24 or Bun >= 1.3
- Database: SQLite (default), MySQL, or PostgreSQL
- Optional: Redis (for cache, session, queue, realtime, scheduler)
