v0.9.1 is out release notes

Your schema is the API.

Zonai is a batteries-included backend framework for Dart. Define your tables, write your rules, and get a complete REST API — auth, live query streams, file uploads, email, and cron — compiled into a single binary you host yourself.

macOS · Linux · Windows · one self-extracting binary · all downloads

lib/src/schemas/tasks.dart you write this
final class TaskTable extends Table<Task> {
  TaskTable(super.$)
    : id = $.id('id', (s) => s.id,
          fromString: TasksId.new, generate: TasksId.generate),
      title = $.text('title', (s) => s.title),
      isComplete = $.boolean('is_complete', (s) => s.isComplete),
      createdAt = $.createdAt('created_at', (s) => s.createdAt),
      updatedAt = $.updatedAt('updated_at', (s) => s.updatedAt);

  final IdColumn<TasksId> id;
  final TextColumn title;
  final BoolColumn isComplete;
  final CreatedAtColumn createdAt;
  final UpdatedAtColumn updatedAt;
}

final tasks = table('tasks', TaskTable.new);
Generated routes zonai handles this
  • POST/dbcreate a row
  • GET/dbread one
  • GET/db/listquery many
  • GET/db/countcount matches
  • PATCH/dbupdate
  • DELETE/dbdelete
  • GET/db/streamlive row
  • GET/db/stream/listlive list
  • GET/db/stream/countlive count
9 endpoints per table · 0 lines of HTTP code · no codegen step
Live queries

Stop polling.

Every table gets three streaming endpoints alongside the ordinary reads. The server holds the connection open and pushes a new payload whenever the underlying query result changes. No WebSocket setup, no pub/sub broker, no separate "realtime" product to bolt on.

lib/widgets/task_list.dart zonai_client
final sub = client.db.listen
    .list(
      body: StreamListBody(
        table: 'tasks',
        where: Eq('isComplete', false),
        limit: 50,
      ),
      fromJson: (row) => row,
    )
    .listen((tasks) {
      // Fires on connect, then again on every insert,
      // update, or delete that changes this result set.
      setState(() => _tasks = tasks);
    });

await sub.cancel();
streaming GET /db/stream/list 0 pushed
  • Ship the migrationtk_9f2a
  • Review auth rulestk_4c81
  • Wire up SMTPtk_7b30
open tasks2

Same rules, same limits

Streams reuse canView / canList / canCount and the matching rate-limit buckets. There is no separate canStream to forget about.

Three shapes

stream-one for a single row, stream-list for a result set, stream-count for a running total. Pick the one your UI actually binds to.

Plain HTTP underneath

GET /db/stream*, JSON in a ?body= query param, events until you cancel. Reachable from anything that speaks HTTP, not just Dart.

Read the streaming guide
Admin dashboard

A control room you did not build.

Every Zonai server serves an admin dashboard at /_ — a data browser, live metrics, cron status and the full auth flow. It is compiled into the same binary as your API, so there is nothing extra to deploy, host, or keep in sync.

localhost:8080/_ preview
Requests · 24h1,043
Errors · 24h 7 0.7% of requests
Active sessions38
p95 response 24 ms within budget
Requests per hourlast 24 hours
00:0012:0023:00
tasks1,204 rows
  • tk_9f2aShip the migrationfalse
  • tk_4c81Review auth rulesfalse
  • tk_7b30Wire up SMTPtrue

Browse and edit every table

Your schema rendered as a data grid — inline edit, row detail, search and filter (including datetime ranges), foreign-key pickers, and photo columns that show the actual image.

Watch the traffic

Requests and errors over the last 24 hours, active sessions, p95 response time, and per-table row counts. Admin traffic can be excluded so your own clicking does not skew it.

Keep an eye on jobs

Which cron jobs exist, which are running right now, and the errors coming up most often — without opening a terminal or tailing a log.

Exercise the auth flows

Sign-in, one-time passcodes, magic links, password reset and email verification are all real screens, so you can try the flow you just configured.

Built with Jaspr, same as this page. Metrics come from GET /dashboard/metrics, so anything the dashboard shows you can also pull yourself.

In the box

The parts you would have written anyway.

Zonai is opinionated so the boring half of a backend is already decided. Everything below is part of the framework, not a plugin you go shopping for.

A REST API per table

Create, read, list, count, update, delete and three live-stream routes, handled straight from your schema. No handler code and no generation step.

Authentication included

Password sign-up/sign-in, one-time passcodes, and magic links — each a single trait mixed into an auth table. Sessions, refresh, and logout come with it.

Docs

Rules before SQL

Table and row rules are plain Dart returning true or false. They run ahead of the query, so a denial costs a 403 and nothing else.

Docs

Live query streams

GET /db/stream, /db/stream/list and /db/stream/count push new payloads as SQLite changes. In Dart, that is client.db.listen.

Docs

Built-in admin dashboard

Served at /_ from the same binary: browse and edit every table, watch requests and errors, check cron status, and run the auth flows. Nothing extra to deploy.

Generated Dart client

zonai_client wraps auth, admin auth, db, photos, and email so your Flutter app never hand-rolls an HTTP call or a JSON map.

Docs

Transactional email

SMTP-backed HTML templates with Mustache variables, sent from lifecycle hooks. Built-in templates for the auth flows, custom ones for everything else.

Push notifications

Send from a lifecycle hook to a recipient set named by a query, not a list of tokens. The fan-out pages, checkpoints, resumes after a restart, and clears dead tokens — FCM for Android, APNs direct for iOS.

Docs

Scheduled jobs

Cron-syntax jobs compiled into their own worker, with the full database API and catch-up logic for runs missed while the server was down.

Per-IP rate limiting

A policy class per table and operation, with dedicated buckets for the auth routes and trusted-proxy handling for real client IPs.

One binary to deploy

./zonai build links your project into build/zonai. Cross-compile it, copy it to a host, run it. SQLite is bundled; nothing else is required.

Docs
How it works

Compiled Dart, all the way down.

There is no interpreter on the request path. Your operations and rules are linked into the server binary; config, extensions, rate limits, and crons compile into separate workers. Every request walks the same ordered pipeline.

HTTP Request
Rate Limit worker Per-IP policy, per table and operation.
Rules in-process Returns true or false. A no is a 403 before any SQL.
Operations in-process Your business logic, linked into the binary.
SQLite engine The query actually executes.
Extensions worker Lifecycle hooks and side effects: email, mutations, fan-out.
Response
403

Denied requests never touch the database

Rules run before any SQL is executed. If a rule returns false the request is rejected immediately, with zero database access and nothing to roll back.

AOT

One binary to ship

./zonai build produces build/zonai with your project linked in for the CRUD hot path. Copy it to a server and run it — no runtime dependencies, no Dart SDK on the box.

Walk through the full pipeline
The tour

Four files and a CLI.

A Zonai project is mostly schemas and rules. Everything else — routing, serialization, session handling, migrations — is the framework’s problem.

lib/src/rules/tasks_table_rules.dart
TaskTableRules main() => TaskTableRules();

final class TaskTableRules extends TableRules<TaskTable, Task> {
  TaskTableRules() : super(tasks);

  @override
  Future<bool> canList(Jwt? jwt) async => true;

  @override
  Future<bool> canView(Jwt? jwt) async => true;

  @override
  Future<bool> canCreate(Jwt? jwt) async => jwt != null;

  @override
  Future<bool> canUpdate(Jwt? jwt) async => jwt != null;

  @override
  Future<bool> canDelete(Jwt? jwt) async => jwt?.admin.isAdmin ?? false;
}

// Anything you do not override defaults to false. Deny by default.

Rules are evaluated before any SQL runs. Every method you do not override denies by default, so a forgotten rule fails closed rather than leaking a table.

Straight answers

What Zonai is not.

Every framework has a shape, and pretending otherwise wastes your afternoon. Here is where Zonai stops, so you can decide before you install anything.

Not a full application framework

Zonai is an API server. It renders no HTML and has no view layer. You talk to it from Flutter or Dart with zonai_client, or over plain HTTP from anything else.

Not a managed cloud service

There is no dashboard to sign up for and no per-seat bill. You host the binary yourself, anywhere that runs Linux, macOS, or Windows.

Not a general-purpose ORM

It is opinionated about how APIs are shaped and it uses SQLite. If you need arbitrary joins across Postgres, this is the wrong tool and that is fine.

Not poll-only for live UI

The live path is /db/stream* and client.db.listen. If you find yourself writing a Timer.periodic against a Zonai backend, you have taken a wrong turn.

Download

Every platform, one binary.

The CLI runs from your project root. Grab the universal build and it works out which OS and architecture you are on by itself; or take the exact one you need.

macOS + Linux

Universal — detects your OS and architecture

34.9 MiB

One self-extracting file containing macOS arm64, macOS x64, Linux x64 and Linux arm64. It caches the right build on first run. Not a Windows executable — Windows takes the zip below.

Windows

x64

12.0 MiB

Extract the zip and put zonai.exe in your project root.

All builds

Already installed? ./zonai version check tells you if there is a newer build, and ./zonai version update replaces the binary in place.

Quick start

Running in a couple of minutes.

Download the binary, define a table, serve. No global install, no Docker, no account. The long version, with the full schema and rules, is in the docs.

  1. 1

    Drop in the binary

    A Zonai app is an ordinary Dart package. Add the schema library, then put the zonai executable in the project root — it is not a pub dependency, and it resolves your project relative to where it sits.

    $ dart create my_app && cd my_app
    $ dart pub add zonai_schema
    $ curl -fsSL https://github.com/mrgnhnt96/zonai/releases/download/v0.9.1/zonai -o zonai && chmod +x zonai
  2. 2

    Define tables and start dev

    Write your tables under lib/src/schemas/, then start the dev server. It watches worker sources, recompiles them, and gives you a TUI dashboard.

    $ ./zonai dev
    # no zonai.yaml yet? dev walks you through creating one
  3. 3

    Migrate and serve

    Generate a migration from your schema, apply it, and the REST API — including the stream routes — is live.

    $ ./zonai db migrate generate -n init
    $ ./zonai db migrate apply
    $ ./zonai serve
For coding agents

Your assistant can read the manual too

A curated index for LLMs lives at llms.txt. Inside a project, ./zonai ai writes framework reference sheets into the repo for Claude Code, Cursor, Copilot, Windsurf, and Cline — so every developer and every agent shares the same context about how your backend is built.

$ ./zonai ai all      # every supported tool
$ ./zonai ai claude   # CLAUDE.md
$ ./zonai ai cursor   # .cursor/rules/zonai-*.mdc

Write the backend in the language you already ship.

Zonai is open source under the MIT license, versioned, and used in production by the people who build it. Clone it, read it, break it.