Reference

Versioning

One major across every package, what a version change means, and how a release is cut.

Every Minnow package shares a major version and moves independently inside it. Each adapter names the oldest engine it can use and accepts later engines inside that shared major. For example, the current @minnowdb/kysely release accepts @minnowdb/core from 0.9.0 up to, but not including, 1.0.0. The same rule covers the export and devtools packages; the React hook is structurally typed and has no engine runtime dependency.

npm install @minnowdb/core @minnowdb/kysely

The Kysely dialect and devtools console use the engine's public APIs. A change that stops those APIs from working is a major change, so each package depends on its siblings by a range that spans the major and stops at it:

"dependencies": { "@minnowdb/core": ">=0.9.0 <1.0.0" }

The ceiling is the compatibility line, and npm refuses a mixed-major install on its own. The floor is the sibling release this package was built against — when an adapter starts using something the engine added in 0.6.8, its next release says >=0.6.8 and npm resolves an engine that has it. Below the major, a fix to the console does not drag the engine's version along with it.

What 0.x means

Minnow is in 0.x, and the usual 0.x rule applies: breaking changes can land in a minor release. Pin exact versions in an application, and read the changelog before upgrading. The smaller API used between Minnow's own packages remains compatible inside the shared major; that is what lets an older adapter accept a newer engine.

A change is breaking when it is one of these:

  • The API. An export removed or renamed, a signature narrowed, or a default changed.
  • SQL. A form that used to run and no longer does, or one that runs and now answers differently. Additions to the feature matrix are not breaking.
  • Stored data. A database written by the old version that the new one cannot open. The locked byte contracts below narrow this category: a release cannot reinterpret or drop an existing block-format-2, snapshot-format-1, IndexedDB-schema-1/2/3, or OPFS-layout-6/7/8 reader or automatic converter.

From 1.0 onwards these move the major version — every package's, together — and minor releases stay additive.

Every published subpath participates in that rule, including low-level extension, testing, worker, and JSON metadata entry points. scripts/api-contract.policy.json assigns each one an audience; the label explains its intended caller and does not weaken its compatibility guarantee. npm run api:check fails on an unclassified subpath, a declaration change, or a change from type-only to runtime reachability. See the v1 support policy for the full contract and release-candidate checklist.

Release notes

The changelog is the release-by-release record. Each entry names the exact packages published together and its additions and fixes; entries since 0.5.0 also record required migration steps and stored-format compatibility. npm run version:check refuses a workspace whose current package versions are not recorded there.

The block format has its own version

The version on the package describes the code. The stored bytes carry a separate BLOCK_FORMAT_VERSION, and a snapshot file carries SNAPSHOT_FORMAT_VERSION. They move on their own schedule — most package releases do not touch them.

Block format 2 is the first locked envelope: a 44-byte header followed by canonical metadata and the stored payload. Permanent raw vectors freeze every Minnow-owned byte for all four physical types. A future writer uses a new format number and retains the format-2 reader rather than changing those bytes in place.

Snapshot format 1 is locked with block format 2 embedded inside it. Its fixture freezes the file framing, canonical header, payload order, and logical block bytes. Native CompressionStream implementations may produce different valid gzip streams, so compressed payload sizes and their derived stored-byte checksums are compared after decompression. An incompatible snapshot writer must use a new snapshot format number and keep the format-1 reader.

IndexedDB schema 1 was its first locked native layout; schema 2 (core 0.10.0) moved each transaction's artifact journal into chunked records and migrates a schema-1 database in place on open. Schema 3 (core 0.13.0) changes no stored record: it admits compaction jobs with replayed merge plans, so a schema-2 build, which cannot read them, refuses the database. Schema 4 (core 0.14.0) rewrites nothing either: it lets one commit store an indexed column's changes as several part records, so a commit's index changes have no size limit, and a schema-3 build refuses the database. No pre-contract schema readers or recreation policy ship. Every future integer schema version must add one ordered migration to the explicit registry and a frozen fixture for the version it supersedes. The browser runs the complete chain inside one versionchange transaction, so either every schema and data step commits or the old database remains intact. An older application build refuses a newer database without mutation.

OPFS native layout 9 (core 0.14.0) is the current contract. Like layout 8 (core 0.13.0) it keeps layout 7's bytes — the checkpoint, WAL, and extents still carry encoding version 7. Layout 8 admits compaction jobs with replayed merge plans, which a layout-7 build cannot read. Layout 9 admits a WAL entry whose payload spans several frames, for a commit too large for one, and checkpointed index changes of any size, which a layout-8 build cannot read. The format marker is the barrier, and upgrading a layout-7 or layout-8 database rewrites only that marker. Layout 7's fixture freezes the format marker, checkpoint, WAL tail, extent payloads, and a separate checksummed acknowledgement file. That file records which WAL boundary completed a strict flush, so recovery can distinguish an unwritten suffix from damage to an acknowledged write. Missing or corrupt acknowledgement bytes stop recovery.

Supported upgrades are automatic, including during 0.x. Opening a layout-6 database with the current build validates its native data and converts it under exclusive ownership; a layout-7 or layout-8 database only has its marker rewritten. All of them end at layout 9. No export/import or separate migration call is needed. Block format 2 and snapshot format 1 are unchanged. Frozen older immutable posting envelopes remain readable. Older application builds refuse the upgraded database.

Conversion prepares and validates a temporary copy before replacing native control files. It needs temporary quota for that copy. The new format marker is flushed before current-format checkpoints replace the old ones; both checkpoint mirrors and the acknowledgement file are durable before the old WAL is reset. A checksummed ready record resumes interrupted publication, and a durable completion receipt prevents a stale conversion record from rolling back later writes. Cleanup follows current-layout recovery and full integrity validation.

A missing payload, corrupt legacy checkpoint, or ambiguous incomplete legacy WAL stops the upgrade with an error. Layout 6 never recorded strict acknowledgement boundaries, so conversion cannot reconstruct lost historical acknowledgement evidence or silently excuse damaged bytes. If an older connection still owns the database, close it and retry opening. Quota or I/O failures also report an error; opening again retries or resumes the conversion.

Every future supported format change must retain a reader or an automatic, crash-resumable conversion through the ordinary open/load API. Manual export/import is not its upgrade path. Tests must open every supported frozen older fixture, preserve its data, continue writing and reopen, and exercise interruption, power loss, concurrent openers and older-writer refusal. Unknown versions are refused without guessing their shape or deleting their data.

There is deliberately no data migration for unsupported pre-v1 prototypes. They were removed before the schema-1 IndexedDB contract was frozen. OPFS always rejects an unknown layout marker; to keep experimental OPFS data, use the build that can still open it to export a supported framed snapshot before upgrading.

Every released block/snapshot pair keeps a frozen database in packages/core/format-fixtures/, opened and queried on every test run. Native OPFS layouts keep a separate frozen file tree in the same directory. The native test enumerates every layout fixture, requires one for the current writer, and reopens and continues writing each supported older tree. Every fixture records the exact @minnowdb/core package version that wrote it, so the suite proves which released writer the compatibility case represents and rejects impossible future provenance. A change that would make either kind unreadable fails in CI rather than in an application. See Testing for how that suite works.

How a release is cut

One command writes the release version, its current sibling dependency floors, the private site manifest, and the lockfile:

npm run version:set -- minor @minnowdb/core   # one package, inside the shared major
npm run version:set -- major                  # every package to the next major, together

Add the release and its migration/storage notes to apps/site/content/docs/changelog.mdx. The version check fails until every current published package version appears there.

Run the full local gate and open a pull request on a release branch:

npm run check:release   # fullest local gate; CI splits release checks across workflows
# Stage the reviewed changes, then:
git commit -m "Prepare core 0.12.1"
git push -u origin HEAD

The pull request runs CI and .github/workflows/release-readiness.yml, which adds the full SQL, simulator and three-browser conformance campaigns with read-only repository permissions. It does not publish. Check both workflows before merging. npm run release:publish -- --dry-run checks the registry and packed package contents without publishing or writing tags.

Publishing is driven by the versions in the manifests, not by a tag or a command. Merging or pushing a version bump to main starts the release automatically. When that commit's CI run goes green, .github/workflows/release.yml checks the same commit with the full SQL and simulator campaigns, including native IndexedDB and OPFS in Chromium, Firefox, and macOS WebKit. Only after those pass does it publish every package whose version npm does not already have and tag each one, such as @minnowdb/core@0.5.0. A push that changes no version publishes nothing, and a rerun after a failure is safe, because the registry decides what has already shipped.

There is no npm token in the repository. npm is configured to trust that workflow in this repository, and hands it a credential that lives for the length of one publish — which is also what signs the provenance attestation you can see on each version's npm page. Nothing expires and there is nothing to rotate. It is configured once per package, in the package's settings on npm, and a package has to exist before it can be configured: the first release of a new package is published from a machine with npm run release:publish, and every release after it from CI.

Two things are checked before anything leaves the machine. npm run version:check proves the workspace agrees with itself — matching majors, ranges that span them, and a docs version that matches the engine — as the first step of both npm run check and CI. And the publish refuses a tarball carrying anything that is not part of the package: tests compile into dist beside the code, and files has to exclude them.

The prepack hooks trace public type exports and exclude unreachable internal declarations through a generated dist/.npmignore. It keeps the local declarations intact for incremental builds and the docs editor. Core strips JavaScript comments and compacts whitespace without renaming identifiers, and devtools compacts its embedded CSS, while public type documentation remains available. Run the packed-consumer test after changing this preparation so both TypeScript module resolvers and the browser exercise the files users actually install.

Where each version's documentation lives

The documentation describes the engine, so it is listed by compatibility line: 0.x, 1.x, 2.x, and so on. The unprefixed site always documents the current line. A minor release inside 0.x can still break a pre-1.0 API, but there is no separate archive for every experimental minor. The final 0.x documentation is frozen only when 1.x replaces it.

URLWhat it serves
minnowdb.com/docs/…The current major line. A link written here follows releases.
minnowdb.com/v0/docs/…The final 0.x documentation, frozen when 1.x is published.

The picker at the top of the docs sidebar moves between them and keeps you on the same page. It reads /versions.json from the site's root rather than the copy compiled into the build, so an archived version still lists releases that did not exist when it was frozen.

Everything under an archived prefix is versioned with it: its search index, its markdown and llms.txt, and every link between its pages. Archived pages also carry noindex, so a search engine offers the current documentation first. The unprefixed console and benchmarks always run the current release.

An archive is the same site built from the final tag in that major line with a base path:

previous_core_tag=$(git tag --list '@minnowdb/core@*' --sort=-version:refname | head -n 1)
git worktree add ../minnow-v0 "$previous_core_tag"
cd ../minnow-v0 && npm ci
SITE_BASE_PATH=/v0 npm run site:build

Before building the new major, copy that frozen apps/site/out/ into apps/site/public/v0/ on main and add the exact final version to the archived list in apps/site/public/versions.json. The next static export carries the archive with it, so the one Vercel project serves both /docs/… and /v0/docs/…. Repeat once per major line; do not add an entry until its files are present in the same deployment.

On this page