Skip to content

Access & identity

The Worker verifies short-lived signed workspace claims. Your application implements the identity adapter that resolves the signed-in subject and their workspace role. Production refuses the development claim route.

Each model’s actions take one of three decisions:

Rule Purpose
role(...) Require a minimum role before SQL runs.
filter(...) Apply a read rule inside SQL queries and counts.
check(...) Evaluate a row or write with application-defined logic.
board: model({
name: t.string(),
ownerId: t.string().index(),
}).access({
read: filter(({ auth }) => ({ ownerId: auth.sub })),
update: check(({ auth, row }) => row?.ownerId === auth.sub),
delete: role("moderator"),
}),

Defaults: read member, create/update editor, delete moderator.

  • filter(({ auth }) => where) is for read only. It is compiled into the SQL of every query that touches the model, so it uses indexes, fills every page and counts correctly. An array is OR; the caller’s own filter ANDs with it.
  • check() works for any action. It receives { auth, row, data, changed } and returns a boolean. Write checks run inside the transaction next to the write they gate. On read they scan matching rows until the page is full or the result set is exhausted. Counts apply the same visibility rule.
  • A rule that throws denies access.

Prefer SQL filters for read visibility, because a function-based read check runs after fetching.

The engine verifies a claim and never issues one. Implement two methods and the library serves the claim route:

import { defineTapeDB, WORKSPACE_ROLE_LEVELS } from "tapedb/worker";
import schema from "./tapedb.schema";
import { migrations } from "./tapedb/migrations.gen";
const tapedb = defineTapeDB({
schema,
migrations,
identity: {
async getSubject(headers) {
const session = await auth.api.getSession({ headers });
return session ? { sub: session.user.id } : null;
},
async getRole(workspaceId, sub) {
const member = await memberships.get(workspaceId, sub);
return member ? WORKSPACE_ROLE_LEVELS[member.role] : null;
},
},
});
export const WorkspaceRoom = tapedb.WorkspaceRoom;
export default tapedb.worker;

POST /_tapedb/claim returns { token, workspaceId, roleLevel, expiresAt }. The client sends it as Authorization: Bearer <token> and renews on reconnect. In development, tapedb dev enables a dev claim route so the client needs no session at all; ENVIRONMENT=production refuses that route, and tapedb deploy refuses to ship the flag.

Schema changes and their SQL migrations are covered in Migrations.