Releases: mrgnhnt96/zonai
Release list
Zonai v0.9.1
- Every refusal now tells a client when to come back. A rate-limited
429
carriesretry-afteralongsidex-ratelimit-limit,x-ratelimit-remaining
andx-ratelimit-reset, and its body is now JSON naming the collection and
operation that hit the limit. If you match on the old plain-text body, key
on the status code instead. Both backpressure503s carryretry-after
too. The one refusal that deliberately still does not is the email limiter,
which does not know its own window and will not invent one. - A saturated read answers
503, not500. Through 0.9.0 a burst past the
read-concurrency limit escaped uncaught and reached the client as an
unclaimed500with no header and nothing useful in the body. It is now the
same shaped refusal as the write side. Reaching it takes 256 genuinely
concurrent reads, so most callers will never have seen it. - Writes survive concurrent load instead of collapsing. A write now
reserves its queue slot before the identity check and the password hash,
and waits briefly for one instead of being refused the instant the queue is
full. At 100 concurrent creates a release build went from ~82 successful
writes a second with 98% refused to ~1100–3200 a second with none refused,
and p99 latency fell from ~790ms to ~43ms. The behaviour change worth
knowing: a write that used to fail immediately may now wait up to 250ms and
then succeed. The cliff is moved rather than removed — capacity is 64 in
flight plus 64 waiting, so a client past ~128 concurrent writes still meets
backpressure, now with aretry-afterto act on. - The server stops writing a stack trace for every refusal it authored.
Shedding load used to cost about six times more than serving a request,
because each deliberate503formatted and printed a full trace — so
saturation fed itself and a saturated sweep could put 40MB in the serve log.
Fixed upstream inrevali_router5.1.2, which this release requires. If you
parse the serve log, note that a released build now logs nothing for a
refusal: an empty log is no longer evidence that nothing was refused.
Full Changelog: v0.9.0...v0.9.1
Zonai v0.9.0
- Reclaim space on any database, not just the log. The Maintenance card's
"Reclaim log space" is now "Reclaim space", with a picker built from this
deployment's real files and their real reclaimable bytes. Reclaiming the main
database asks for its filename typed first, because aVACUUMthere takes an
exclusive lock on application data.ZonaiDb.reclaimSpaceandPOST /dashboard/maintenance/reclaim-spacetake the target and the floor; the old
reclaim-log-spaceroute stays, redirecting onto the new one and behaving
exactly as it did. zonai compileandzonai buildrefuse a mismatched Dart SDK. They now
exit 1 with a message naming both versions — "zonai was built with Dart
3.13.2; you are on 3.12.0" — instead of producing workers that fail later at
spawn. Every other command warns once and continues, and
--no-dart-sdk-checkturns the check off. This is the one thing here that can
stop a command that used to succeed.- Workers compiled by a Dart SDK that does not match the one zonai was built
with are no longer loaded in-process. That combination could kill the running
host outright with SIGABRT — no exception to catch, every in-flight request
gone with it. The host now decides before the spawn and falls back to the
worker process, which serves identically. zonai compileexits non-zero when a worker fails to compile. It used to
report success on a project full of analyzer errors, andzonai build— which
guards on that exit code — bundled whatever stale executables were already on
disk.zonai db migrateandzonai buildno longer die inside your project with
Couldn't resolve the package 'sqlite3'. The vendored DDL driver reached
package:sqlite3through an export chain nothing called;zonai_schemakeeps
that package a dev_dependency so a query-only client never has to resolve it.- zonai is now built with Dart 3.13.2.
Full Changelog: v0.8.5...v0.9.0
Zonai v0.8.5
- The dashboard's "Most sessions" list is clickable — each user opens the same
row-detail panel the tables screen opens, instead of printing an id to copy. - Long tooltips stay inside the window. They wrap at their authored newlines,
flip on both axes, and measure their real box rather than a hardcoded 44px. - The dashboard scrollbar sits flush against the right edge of the viewport
instead of 20px in from it.
Full Changelog: v0.8.4...v0.8.5
Zonai v0.8.4
- API tokens. A credential that needs no sign-in:
zonai db token create/list/revoke/deletetalks to the database file directly,/admin/tokens
mints one over HTTP, and the dashboard has an API tokens screen (and a
mint-a-bound-token action on an auth row's panel). Tokens are scoped to
tables and operations, admin unless told otherwise, stored as a SHA-256, and
record when they were last used. - Forced password reset. An account can be made to owe a new password —
fromzonai db, from the server, and from the dashboard's own door. A
password sign-in that owes one is refused with a403 password_reset_requiredenvelope, pinned in the swagger and typed in the
client asPasswordResetRequiredException. beforeSignUp.AuthExtensioncan now decline a sign-up instead of only
being told one happened, and the gate runs before the OTP and magic-link
email rather than after.- Push from the dashboard. Select rows in a table with a device-token
column, compose one notification, and send it to every selected device. zonai ai update. Refreshes the reference files a project already has —
which are version-stamped now, so a stale one is visible — without installing
files for tools it never asked for.- Fixes: the reads connection got the
busy_timeouteverything assumed it had;
POST /auth/confirmis rate-limited; a disposed mailman worker no longer
turns a dropped reply into a 10-second hang; a fire-and-forget email send owns
its failure; a row's password reset goes to that row's own table; two
conditionally-rendered auth components no longer break their own teardown;
and the web build recovers from a stale asset graph instead of dying. zonai_schema0.4.2 on pub.dev, with the changelog owed since 0.4.1.
Full Changelog: v0.8.3...v0.8.4
Zonai v0.8.3
Full Changelog: v0.8.2...v0.8.3
Zonai v0.8.2
Full Changelog: v0.8.1...v0.8.2
Zonai v0.8.1
Highlights
zonai gen client — a typed Dart client, generated from your schema
The headline of this release. zonai gen client reads .zonai/schema.json and emits a client where a query that compiles is a query the server accepts.
- Typed rows and column tokens —
PostsRow.titleis aString;Posts.title.eq('x')builds the filter. A column that does not exist is a compile error, not a runtime 400. - Typed expand, both halves —
Posts.expand.authorId.companyIdbuilds the request path with each hop typed by the table it points at, andPostsExpanded.authorIdcomes back as anAuthorsRow?. - The full write surface —
create,createMany,update,updateMany,delete,deleteManyall have typed mirrors. Update fields take aPatch, so "leave alone" and "set to NULL" are different call sites rather than the same nullable argument. MapField.at()— patch one path inside a JSON map column (settings.theme) instead of rewriting the whole column.- Typed live queries —
listen.one,listen.listandlisten.counttake the same column tokens and expand paths as the non-streaming methods. - Enum columns get their own type — an extension type over
Stringwith named constants,valuesandisKnown. A member the server adds after you generate still arrives and still round-trips, which a Dartenumcould not do. - One import — the generated barrel re-exports
zonai_client, soWhere,OrderByTerm,SortDirectionand the rest resolve without a second import. gen client --checkfails when the committed output has drifted from the schema, so a generated client can be committed and kept honest in CI.
Email preheaders
Emailgains apreheader— the line a mail client shows next to the subject. Without one, clients scrape the first visible text, so every message previews as its greeting.- Every shipped template carries a hidden preheader block, hidden four ways so it survives Outlook and clients that strip
display:none, with the zero-width padding that stops the preview running on into a button label. - Each built-in auth email ships a default. The OTP one deliberately leaves the code out — the preheader is what renders on a locked phone.
Dashboard
- Push queue and sessions panels — queue depth by status, the last drain's real outcome, and the three things
_jwtcan honestly support: active, expiring soon, distinct users. - Test-send panel — appears only when a collection declares a
deviceTokencolumn. It reuses the push transport and writes nothing: no job row, no cursor, no token pruning, noonPushRejected. It reports the provider's own words, soUnregistered(app gone) andBadDeviceToken(wrong APNs environment) stop collapsing into one answer.
Fixes
- A 5xx your app authored is delivered again.
revali_routerbelow 5.1.1 replaced every response at or above 500 with a bareInternal Server Errorwhendebug: false— so all six 5xx sites in the exception catcher were dead in a released binary and correct underzonai dev. The floor is now^5.1.1. push(...)works from a user-triggered hook. It was authorized by the caller's identity, and extension requests carry the requesting user's JWT — so the pattern both docs pages prescribe threwTableAccessDeniedExceptionfor every ordinary user. Authorization is now by provenance: only developer-authored Dart in a hook or cron can enqueue. ThedeviceToken-column guard that bounds what a fan-out can read is unchanged.- A table named
fieldno longer breaks the generated barrel. The runtime exports fourteen names now, and any of them can collide with a table name; a clash is refused up front, naming the table and thenames.<table>.rowoverride that fixes it. zonai gen client --checkno longer reports every file as drifted on Windows. Git for Windows checks out CRLF while the generator emits LF, so a committed client looked stale with no way to fix it. Line endings are now normalised for the comparison; anything else is still drift.- The generated expand example is derived from the table's own foreign keys rather than invented.
Where.isNull(column)/Where.isNotNull(column)— const factories so those clauses stay constructible without importingNull, which shadowsdart:core's.
Bundled SQLite (resqlite)
Three defects fixed in the vendored engine; its suite is back to 172 passed, 0 skipped.
db_statuswalked reader handles that were never opened, segfaulting on a NULL dereference.- Readers could not open a database nothing had written to yet, reporting "reader not open" instead of SQLite's own
no such table. - Rows whose schema reports no column names read their real values instead of appearing empty.
Docs and internals
- The typed client is documented on the site, indexed in
llms.txt, and taught to the assistant rules the CLI installs into your project. Ten of its snippets are now compiled against a real generated client, and five more against a second fixture client built to match the page's prose. - Push docs say who can send and the amplification that comes with it, and stop claiming everything routes through FCM when direct APNs shipped.
- Four missing pages added to the site; push notifications added to the feature grid.
- The stress harness gates the error rate and deliberately refuses to gate p99, with the calibration data that says why — three runs put p99 at a 139% spread at the median cell and the error rate at 0.00 points.
- CI parses workflows with Ruby when PyYAML is absent, instead of exiting 1 and silently checking nothing.
- Golden-file tests for the generated client, covering the manifest and orphaned files.
Upgrading
zonai gen clientoutput needszonai_schema0.4.1+ andzonai_client0.2.2+, both published alongside this release. The generated runtime callsWhere.isNull, and its single import relies on thezonai_clientbarrel re-exporting the query vocabulary; neither exists in earlier published versions, and generated code will not compile against them.zonai initnow scaffoldszonai_schema: ^0.4.1; existing projects should bump both.- Re-run
zonai compileafter upgrading. The.protocolstamp records the IPC framing version, not the message vocabulary, so it will not catch a stale worker. - Nothing in this release is breaking for code that does not use the generated client.
Zonai v0.8.0
Highlights
Features
- Sign in with OAuth — Google, Apple, GitHub, Microsoft, Facebook, Discord, GitLab, LinkedIn, plus
OAuthProvider.custom(...). PKCE, token exchange,id_tokenverification, Apple ES256. Addwith OAuthto your auth table and list your providers. - OAuth in the Dart client —
Auth.providers(),startUrl(),complete(),signInWithIdToken(), covering the redirect and native flows. - Admin invites — invite by email, check a link is live without spending it, revoke, list members, remove one (which revokes their sessions). Two dashboard screens and an invite email template.
- Push notifications —
push(...)from any rule, operation or cron, fanned out through a checkpointed job table, so a large send survives a restart and never blocks the request that started it. - APNs without Firebase — a declared platform column routes each device to Apple or Google. Verified against real APNs and FCM on physical devices.
- Maintenance screen — per-table storage metrics, plus purge logs, purge a table, clean up unreferenced photos, reclaim log space.
Security
Seventeen fixes, landed as one sweep:
- Admin role is server-derived on every request — demotion is immediate, and a tampered JWT admin claim is ignored.
- Weak and reused secrets are refused at startup, including
jwtSecretequal topasswordSecret, or either appearing in its ownprevious*Secrets. - Admin auth throttled to 10 requests / 15 minutes, per IP (was the generic 100/min).
wherecan no longer filter on unknown or secret columns — passwords included, onupdateanddeletetoo.POST /emailrequires an admin token. It answered anybody, which made the server an open mail relay.- Exposure closed on CORS, Swagger, server binding and error oracles; plus the photo path guard, the
AsAdminsign-up default, and the dev backdoors. - Default JWT lifetime is 24 hours, down from 14 days.
Fixes
- A cancelled OAuth sign-in returns to the app instead of 400ing at it.
- Apple is asked for
form_post, with the field pinned across the worker boundary. - A checkout with no OAuth credentials boots again.
- One malformed push message could prune every recipient in a batch.
- A missing APNs key failed the whole job rather than the leg that needed it.
- A large fan-out stopped early, and the push config bounds were not enforced.
- Migration
0007regenerated, so the internal snapshot chain is unbroken. - The storage payload reports the database path absolutely, as it promises.
- Maintenance screen styles register for SSR; Swagger covers the storage, cleanup and admin-OAuth routes.
Upgrading
- Needs
zonai_schema0.4.0+ andzonai_client0.2.1+. The floor moved because this CLI sendsAuthType.oauthand theoauthStart/oauthCallback/adminInviterate-limit operations, and an older schema throws on a name it does not have. AuthTypeandRateLimitOperationgained values — an exhaustiveswitchover either stops compiling until you handle them.- Re-run
zonai compile. Your.zonai/executables/*.exekeep the old message vocabulary until rebuilt, and the.protocolstamp will not catch it. POST /emailnow needs an admin token — anything calling it unauthenticated will start getting 403.
Full Changelog: v0.7.2...v0.8.0
Zonai v0.7.2
What's Changed
- ci: the static job never generated the code it analyzes, and e2e never built a binary by @mrgnhnt96 in #30
- chore(deps): bump raindrop to the 2026-08-15 upstream sync by @mrgnhnt96 in #31
Full Changelog: v0.7.1...v0.7.2
Zonai v0.7.1
Full Changelog: v0.7.0...v0.7.1
Fixes the regression in v0.7.0
v0.7.0 returned 500 on every PATCH /db. If you are on 0.7.0, upgrade.
In / NotIn where-clauses could not cross the isolate worker boundary.
serializeWhereValues ended in .cast<Object>(), which returns a CastList
rather than a plain List — and an isolate message may only carry primitives
plus plain List/Map instances, so it was rejected with ArgumentError
before the request was ever dispatched. Released binaries always use the
isolate transport, so this only ever failed there.
The clause had been broken since the isolate transport landed, but nothing put
one on a hot path until v0.7.0's update read-back started keying on it — which
put an In on the update path of every request.
Also upgrade zonai_schema to 0.3.1
The CLI carries its own copy, so this release fixes PATCH /db on its own.
Your workers compile against the zonai_schema your project resolves, and
that copy still has the bug — so a rule, operation or cron calling get.* with
an In/NotIn filter keeps failing until you upgrade:
dependencies:
zonai_schema: ^0.3.1Then re-run zonai compile. Your .zonai/executables/*.exe keep the old
code until rebuilt, and the .protocol stamp will not catch it — that records
the IPC framing version, not what the messages contain.
Verification
Verify Release run 31765112689
was green on all 17 jobs — no exception claimed, unlike v0.7.0. The two
gates excepted for that release both cleared: compat-* self-cleared once
v0.7.0 became the baseline, and cross-run-linux-x64 went green after its
fixture pin was moved off 0.6.1, so its positive control now actually fires.
The released binary was then checked by hand against a clean-slate project on
the isolate transport: where on id, on a non-id column, on a column the
same update rewrites, and a where matching nothing (404, not 500) — all
correct, with the writes verified in the database afterwards.