Skip to main content

Policy conditions on typed columns

The access templates (direct sharing, fixed-role group) cover the common collaboration shapes. When they don't fit, shape the policy yourself with s.definePermissions — the same surface the generated starter uses for its owner-only default. Conditions match on magic columns ($createdBy) and on your own typed columns alike; both are conformance-verified.

Gate rows on an application column

A moderation flow where drafts are private until published, and published rows become undeletable:

// src/permissions.ts
import { s } from "@nzip/lofi/schema";
import { app } from "./schema.ts";

export default s.definePermissions(app, ({ policy, session, anyOf }) => {
policy.posts.allowInsert.always();
// Readable when published, or always by the author.
policy.posts.allowRead.where(anyOf([
{ published: true },
{ $createdBy: session.user_id },
]));
policy.posts.allowUpdate.where({ $createdBy: session.user_id });
// Only unpublished drafts may be deleted.
policy.posts.allowDelete.where({ published: false, $createdBy: session.user_id });
});

anyOf composes alternatives: a row enters live queries when any branch matches, and an object condition with several keys requires all of them. Policy conditions are the filter of record — a row that fails them never reaches the client, so the UI does not need defensive re-checks.

Let the UI ask what it may do

Rather than re-deriving the policy in components, select the permission introspection columns and render from them:

import { s } from "@nzip/lofi/schema";

const posts = useLiveQuery(
() =>
app.schema.posts
.select("title", "published", ...s.permissionIntrospectionColumns)
.where({}),
[],
);
// Each row carries $canRead, $canEdit, and $canDelete, evaluated for the
// current session against the deployed policy.

A row's $canDelete is false exactly when the delete rule above rejects it, so a delete button can disable itself without duplicating the condition. If the policy changes later, the UI follows the deployment instead of drifting.

Test custom policies the same way as the templates: at least two isolated identities, one row that each rule admits and one it rejects, and a rejected mutation surfacing through the mutation hook's error state.