Data and access rules
Define your tables in flatstack/schema.ts. Each table says who may read and write its rows, and Flatstack enforces that for your app’s users.
import { integer, owner, table, text, timestamp } from "@flatstack/server/schema";
export const todos = table("todos", {
id: text().primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: owner(),
title: text().notNull(),
done: integer({ mode: "boolean" }).default(false),
createdAt: timestamp().defaultNow(),
});
Columns are Drizzle columns (text, integer, real, plus timestamp(), owner() and ref(otherTable)), so rows are fully typed.
Access rules
| Rule | Who |
|---|---|
owner |
Signed-in users, only for rows whose owner() column is theirs. |
authenticated |
Any signed-in user. |
public |
Anyone, signed in or not. |
deny |
Nobody (server-only code can still use asAdmin()). |
(ctx, row) => boolean |
Whoever the function allows, checked row by row. |
export const posts = table(
"posts",
{ id: text().primaryKey(), authorId: owner(), title: text().notNull() },
{ access: { read: "public", write: "owner" } },
);
Defaults, when you leave out access:
- A table with an
owner()column is owner-only for reading and writing. - A table without one is closed to your app’s users.
public is never a default: you have to write it.
Querying as the user
In a function, ctx.db.asUser() applies the rules for whoever is calling:
const db = ctx.db.asUser();
await db.insert(todos, { title: "Buy milk" }); // userId is set to the caller
await db.select(todos, { where: eq(todos.done, false), limit: 20 });
await db.update(todos, { done: true }, { where: eq(todos.id, id) });
await db.delete(todos, { where: eq(todos.id, id) });
- Owner tables: inserts set the owner column to the caller. Naming someone else as the owner, or moving a row to another owner, is refused with
403 forbidden. - Updates and deletes only touch rows the caller may write. Rows they can’t write are left alone, and aren’t returned.
- A rule that needs a signed-in user answers
401 unauthenticatedwhen nobody is signed in. - With a function rule, reads are filtered after the query, so a page can come back with fewer rows than
limit. Writes are all or nothing: one row the rule rejects refuses the whole write.
asUser() covers single-table queries. For joins or raw SQL, use ctx.db.asAdmin(), which ignores access rules. Use it only in server-only code.
The data API
Add expose: true to serve a table at /api/data/<table> under these same rules. See Data API.
As Markdown: apps/data.md