LogoZonai

Table Rules

Controlling which operations are allowed on a table based on the JWT.

Table rules control whether a JWT is permitted to perform an operation on a table. They run before any SQL, and they do not see individual rows.

Creating Table Rules#

Create <table>_table_rules.dart in rulesPath and extend TableRules:

import 'package:my_app/src/schemas/tasks.dart';
import 'package:zonai_schema/zonai_schema.dart';

TaskTableRules main() => TaskTableRules();

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 => true;
  @override
  Future<bool> canCount(Jwt? jwt) async => true;
  @override
  Future<bool> canView(Jwt? jwt) async => true;
  @override
  Future<bool> canUpdate(Jwt? jwt) async => jwt != null;
  @override
  Future<bool> canDelete(Jwt? jwt) async => jwt?.admin.isAdmin ?? false;
}

All unoverridden methods default to false (deny).

Available Methods#

MethodEndpoint Checked BeforeDefault
canCreate(jwt)POST /dbfalse
canList(jwt) GET /db/list, GET /db/stream/list false
canCount(jwt) GET /db/count, GET /db/stream/count false
canView(jwt) GET /db, GET /db/stream false
canUpdate(jwt)PATCH /dbfalse
canDelete(jwt)DELETE /dbfalse

Streaming (/db/stream*) reuses the same canView / canList / canCount checks as ordinary reads — there is no separate canStream* method. See Streaming.

For view, update, and delete: the table rule runs first, then row rules run (if the table rule passes).

Common Patterns#

// Public read, authenticated write
@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 != null;

// Admin-only
@override Future<bool> canDelete(Jwt? jwt) async => jwt?.admin.isAdmin ?? false;

// Fully public (no file needed — but explicit is fine too)
@override Future<bool> canList(Jwt? jwt) async => true;
// etc.

Accessing Custom JWT Claims#

Custom claims added via Auth Operations are available via jwt?.claims:

@override
Future<bool> canCreate(Jwt? jwt) async {
  return jwt?.claims['plan'] == 'pro';
}

See JWT Claims for all available fields.

Custom Operations#

Named operations that aren't create/update/delete/view/list/count — TableOperations.custom(operation, ...) — go through customOperations, not the methods above. An operation name that isn't a key in the map is denied, same as any unoverridden method:

@override
Map<String, CustomTableOperationRule> get customOperations => {
  'archive': (jwt) async => jwt?.admin.canEdit ?? false,
};

Row rules need a matching entry too — see Row Rules: Custom Operations.