Skip to content

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 unauthenticated when 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