clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

fix/175-template-quality-pass
lofi / docs / examples / policy-conditions.md
policy-conditions.md 2.3K rendered
1# Policy conditions on typed columns23The access templates ([direct sharing](shared.md), [fixed-role group](group.md)) cover the common4collaboration shapes. When they don't fit, shape the policy yourself with `s.definePermissions` —5the same surface the generated starter uses for its owner-only default. Conditions match on magic6columns (`$createdBy`) and on your own typed columns alike; both are conformance-verified.78## Gate rows on an application column910A moderation flow where drafts are private until published, and published rows become undeletable:1112```ts13// src/permissions.ts14import { s } from "@nzip/lofi/schema";15import { app } from "./schema.ts";1617export default s.definePermissions(app, ({ policy, session, anyOf }) => {18 policy.posts.allowInsert.always();19 // Readable when published, or always by the author.20 policy.posts.allowRead.where(anyOf([21 { published: true },22 { $createdBy: session.user_id },23 ]));24 policy.posts.allowUpdate.where({ $createdBy: session.user_id });25 // Only unpublished drafts may be deleted.26 policy.posts.allowDelete.where({ published: false, $createdBy: session.user_id });27});28```2930`anyOf` composes alternatives: a row enters live queries when any branch matches, and an object31condition with several keys requires all of them. Policy conditions are the filter of record — a row32that fails them never reaches the client, so the UI does not need defensive re-checks.3334## Let the UI ask what it may do3536Rather than re-deriving the policy in components, select the permission introspection columns and37render from them:3839```ts40import { s } from "@nzip/lofi/schema";4142const posts = useLiveQuery(43 () =>44 app.schema.posts45 .select("title", "published", ...s.permissionIntrospectionColumns)46 .where({}),47 [],48);49// Each row carries $canRead, $canEdit, and $canDelete, evaluated for the50// current session against the deployed policy.51```5253A row's `$canDelete` is `false` exactly when the delete rule above rejects it, so a delete button54can disable itself without duplicating the condition. If the policy changes later, the UI follows55the deployment instead of drifting.5657Test custom policies the same way as the templates: at least two isolated identities, one row that58each rule admits and one it rejects, and a rejected mutation surfacing through the mutation hook's59`error` state.