This guide walks through creating a small REST API with a users auth table and a tasks
table. By the end you will have a running server and be able to sign up, sign in, and create tasks.
Step 1: Create a Dart Project#
dart create my_app && cd my_app
Add Zonai to pubspec.yaml:
dependencies:
zonai_schema: ^0.1.0
dev_dependencies:
zonai: ^0.1.0
Run dart pub get.
Step 2: Initialize Zonai#
zonai dev
If no zonai.yaml exists, zonai dev prompts you through creating one
Step 3: Define Tables#
Create lib/src/schemas/users.dart:
import 'package:zonai_schema/zonai_schema.dart';
final class User {
const User({
required this.id,
required this.email,
required this.isVerified,
required this.createdAt,
required this.updatedAt,
});
final UsersId id;
final String email;
final bool isVerified;
final DateTime createdAt;
final DateTime updatedAt;
}
class UsersId extends Id {
const UsersId(super.value);
factory UsersId.generate() => UsersId(Id.generate('us'));
}
final class UserTable extends AuthTable<User> with PasswordAuth {
UserTable(super.$)
: id = $.id('id', (s) => s.id, fromString: UsersId.new, generate: UsersId.generate),
email = $.email('email', (s) => s.email),
isVerified = $.isVerified('is_verified', (s) => s.isVerified),
createdAt = $.createdAt('created_at', (s) => s.createdAt),
updatedAt = $.updatedAt('updated_at', (s) => s.updatedAt);
@override
User fromRow(RowReader read) => User(
id: read(id), email: read(email), isVerified: read(isVerified),
createdAt: read(createdAt), updatedAt: read(updatedAt),
);
final IdColumn<UsersId> id;
final EmailColumn email;
final IsVerifiedColumn isVerified;
final CreatedAtColumn createdAt;
final UpdatedAtColumn updatedAt;
}
final users = authTable('users', UserTable.new);
Create lib/src/schemas/tasks.dart:
import 'package:zonai_schema/zonai_schema.dart';
final class Task {
const Task({
required this.id,
required this.title,
required this.isComplete,
required this.createdAt,
required this.updatedAt,
});
final TasksId id;
final String title;
final bool isComplete;
final DateTime createdAt;
final DateTime updatedAt;
}
class TasksId extends Id {
const TasksId(super.value);
factory TasksId.generate() => TasksId(Id.generate('tk'));
}
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);
@override
Task fromRow(RowReader read) => Task(
id: read(id), title: read(title), isComplete: read(isComplete),
createdAt: read(createdAt), updatedAt: read(updatedAt),
);
final IdColumn<TasksId> id;
final TextColumn title;
final BoolColumn isComplete;
final CreatedAtColumn createdAt;
final UpdatedAtColumn updatedAt;
}
final tasks = table('tasks', TaskTable.new);
Step 4: Configure the App#
Create lib/src/config/db_config.dart:
import 'package:zonai_schema/zonai_schema.dart';
AppConfig main() {
return AppConfig(
appName: 'My App',
jwtSecret: const String.fromEnvironment('JWT_SECRET'),
passwordSecret: const String.fromEnvironment('PASSWORD_SECRET'),
baseUrl: 'http://localhost:8080',
);
}
Create .env in the project root:
JWT_SECRET=my-dev-jwt-secret-at-least-32-chars
PASSWORD_SECRET=my-dev-password-secret-different-value
Add .env to .gitignore.
Step 5: Add Access Rules#
Create lib/src/rules/task_table_rules.dart:
import 'package:zonai_schema/zonai_schema.dart';
import 'package:my_app/src/schemas/tasks.dart';
final class TaskTableRules extends TableRules<TaskTable, Task> {
TaskTableRules() : super(tasks);
@override
Future<bool> canCreate(Jwt? jwt) async => jwt != null;
@override
Future<bool> canList(Jwt? jwt) async => jwt != null;
@override
Future<bool> canView(Jwt? jwt) async => jwt != null;
@override
Future<bool> canUpdate(Jwt? jwt) async => jwt != null;
@override
Future<bool> canDelete(Jwt? jwt) async => jwt != null;
}
Without a rules file, all operations on a table are denied. Auth endpoints use auth-specific rules (canSignUp, canSignIn, etc.) in addition to CRUD rules — and they still require a rules file. If you omit it, sign-up and sign-in will be denied.
Step 6: Start the Dev Server#
zonai dev
zonai dev is the recommended command during development. It starts the server and opens an interactive TUI with helpers for migrations, schema inspection, and more.
To start the server without the TUI, use zonai serve. This is useful when you want quieter output or want to replicate a closer-to-production environment while still in development.
Step 7: Make API Calls#
Sign up:
curl -X POST http://localhost:8080/auth/sign-up \
-H "Content-Type: application/json" \
-d '{"type":"signUp","table":"users","email":"alice@example.com","password":"hunter2"}'
Sign in:
curl -X POST http://localhost:8080/auth/sign-in \
-H "Content-Type: application/json" \
-d '{"type":"signIn","table":"users","email":"alice@example.com","password":"hunter2"}'
The response includes data.accessToken. Use it for subsequent requests. The table name is always in the JSON body — never in the URL path.
TOKEN="eyJ..."
# Create a task
curl -X POST http://localhost:8080/db \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"table":"tasks","object":{"title":"Buy groceries","isComplete":false}}'
# List tasks
curl -G http://localhost:8080/db/list \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode 'body={"table":"tasks","limit":20}'
Step 8: Stream Live Updates (Do Not Poll)#
Zonai pushes query results over a long-lived HTTP connection whenever rows change. Prefer this over a timer. In Dart apps use
zonai_client → client.db.listen (see Streaming).
# Keep this curl open — a new JSON payload arrives when matching rows change
curl -N -G http://localhost:8080/db/stream/list \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode 'body={"table":"tasks","where":{"isComplete":{"eq":false}},"limit":20}'
Search docs for stream / listen, not "realtime" or "SSE". Every table gets /db/stream, /db/stream/list, and /db/stream/count automatically.
Next Steps#
-
Live Queries (Streaming) —
client.db.listenand stream endpoints - Project Structure — understand what each directory does
- Schemas — all column types and modifiers
- Rules — fine-grained authorization
- Dart Client — typed client including
db.listen
