Upgrade Guide
This guide explains how to move a Zerotal app to a newer release. For the list of what changed in each version, see the Release Notes.
Versioning
Zerotal follows semantic versioning across its @zerotal/* packages, which share a
version line:
- Patch (
x.y.Z) — bug fixes, safe to take anytime. - Minor (
x.Y.z) — new features, backward compatible. - Major (
X.y.z) — breaking changes; read the version's section in the Release Notes before upgrading.
Warning — while the 1.x line is young, a breaking change may also land in a minor or a patch. It is always labelled BREAKING in the Release Notes with migration steps. Read the notes for every version you cross, not only the majors. See Releases and versioning for which ones have shipped and when this carve-out ends.
Warning — always upgrade the
@zerotal/*packages together. Mixing versions across core, ORM, and feature packages leads to type and runtime mismatches.
Upgrade steps
-
Bump the packages. Update every
@zerotal/*dependency to the target version, then reinstall:bun update # within the ranges in package.json # or pin exact versions, then: bun install -
Review the breaking changes. For a major release, work through its section in the Release Notes and apply each migration note.
-
Run migrations. A release may add framework tables or columns:
bun zt migrate -
Type-check and test. The fastest way to surface breaking API changes:
bun run typecheck bun test -
Boot it. Start the dev server and exercise the main flows:
bun dev
Pre-release checkout to 1.0
1.0 is the first public release, so there is no earlier published version to move from. If you have been building against a pre-release checkout of the source, these are the changes that need action. Full detail is in the 1.0.0 release notes.
-
Import storage from
@zerotal/core/storage. The separate@zerotal/storagepackage is gone — file storage ships inside core, as a subpath besidecore/loggerandcore/http. Drop the dependency frompackage.jsonand update the imports:// before import { Storage, StorageProvider } from "@zerotal/storage"; // after import { Storage, StorageProvider } from "zerotal/storage";Nothing else changes: the same
StorageProvider, the sameconfig/storage.ts, the same disks and driver API. -
Move public files to
storage/public. The public disk's root changed fromstorage/app/publictostorage/public, and it is served at/storage/publicrather than/storage. Everything else under the storage root is private: a local disk outsidestorage/publiccan only be served withserve: { signed: true }, and serving one openly now fails at boot.mv storage/app/public storage/publicAny hardcoded
/storage/...link becomes/storage/public/.... Better, ask for the URL instead:await Storage.publicUrl(path, { disk: "public" }). -
Expect logs on disk. Every entry now goes to the terminal and a date-rotated file under
./storage/logs, kept 14 days. If you ship stdout to a collector and want no files, setfile: falseinconfig/logging.ts. If you had adefault: "daily"channel to get files, you can delete it — and your terminal output comes back, since console is no longer a channel thatdefaultcan point away from. -
Add a
fillable(orguarded) list to every model written from user input. Models now guard mass assignment by default — a model declaring neither rejects every attribute passed tocreate()/fill()with aMassAssignmentError. For trusted, non-user writes useforceFill()/forceCreate(), wrap a block inModel.withoutGuard(fn), or setstatic unguarded = true. -
Ensure
APP_KEYis set in every environment.localstoragetemporaryUrl()now throwsStorageKeyMissingErrorinstead of falling back to a hard-coded key, and a key under 32 bytes now throws at boot in production-like environments (APP_ENV=production/prod/staging). Generate one withbun zt key:generate. -
Update merged-package imports.
@zerotal/lock→@zerotal/core/lockand@zerotal/logger→@zerotal/core/logger; remove both frompackage.json. No API changes. -
Re-check ops-surface gates. The devtools inspector, the monitor panel's default open access, and the dev error page now key off
APP_ENVand fail closed — an unset orstagingAPP_ENVno longer exposes them. The admin panel and the monitor/metricsendpoint are now default-deny/opt-in. Set an explicitmiddleware/authinconfig/admin.tsandconfig/monitor.ts, and setAPP_ENV=developmentlocally if you relied on the previous open-in-dev behaviour with an unset value. -
Redis cache prefix moved to
zerotal:cache:. If you run cache and queue on the same Redis DB,Cache.flush()no longer deletes queued jobs. Existing cache entries under the oldzerotal:prefix are effectively invalidated on upgrade (they are simply not read again) — no action needed beyond expecting a cold cache.
1.4 to 1.5
-
Move query values into
route()'s third argument. A param that matches no:segmentused to be appended to the query string, so a typo'd param name produced a wrong URL instead of an error. Params are now exact, and an unknown key throws:route("search", { q: "zerotal", page: 2 }); // before route("search", {}, { q: "zerotal", page: 2 }); // nowThe same applies anywhere params are passed on their own —
redirect().to(name, params),redirectTo(),Url.route(),Uri.route(), and Flow'sredirectRoute(). Where those need a query string, build the URL withroute()and redirect to it.To find them: search for
route(calls whose second argument holds a key that is not a:segmentof that route.bun zt route:listprints the patterns to check against, and after step 2 the type-checker finds the rest for you. -
Generate and commit the route types — this is what turns the change above from a runtime error into a compile error, and it is the point of the release:
bun zt route:types # writes types/routes.generated.tsCommit the file.
zt devrefreshes it on every restart; addbun zt route:types --checkto CI so it cannot go stale. Skipping this step is supported —route()then behaves exactly as it did, minus the query-param change. -
Rebuild the Inertia page registry to get typed page names and props:
bun zt inertia:buildThen fix what it finds. Two are worth expecting: a page whose component declares a prop the controller never passes (add it, or make the prop optional), and a
defer()/optional()prop the component declares as required (make it?— it really is absent on first paint). Declare anyInertia.share()keys of your own on theSharedPropsinterface so pages that read them do not look unpassed; see Typed props.
The managed zt.ts
zt.ts is framework-managed — the header says do not modify. If a release
changes the CLI entry point, re-scaffold it rather than hand-editing. Because you
never customized it, replacing the file is safe; your app lives in app/,
bootstrap/, config/, and routes/.
Things to check after a major upgrade
- Config shapes — a
*Config()factory may have new or renamed fields. Your editor's types will flag mismatches; re-checkconfig/*.tsagainst the Configuration docs. - Provider registration — confirm any package providers you list in
bootstrap/providers.tsstill export the same names. - Deprecations — a minor release may log deprecation warnings for APIs removed in the next major. Resolve them before taking the major.
- Lockfile — commit the updated
bun.lockso deploys install the same versions.
Next steps
- Release Notes — per-version changes and migration notes.
- Configuration — config factories whose shapes may change.
- Getting Started — the baseline project layout.