> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bijection.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Write Rules

> Authorize every change to a table before the transaction commits

A write rule decides whether the changes a transaction makes to a table may
commit. You attach it with `.govern`, and Bijection runs it at commit for every
transaction that inserts, patches, replaces or deletes documents in that
table, whichever mutation made the change.

```ts bijection/schema.ts {4} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
tasks: defineTable({ project: v.string(), title: v.string() })
  .index("by_project", ["project"])
  .govern(makeFunctionReference<"query">("access:taskWrite")),
```

The rule is a query that receives the changes and returns one result per
change:

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { decideEach, readAccessResults } from "bijection/server";
import { v } from "bijection/values";
import { internalQuery } from "./_generated/server";

// Only project members may change a project's tasks, and a task can't be
// moved into a project the caller isn't a member of.
export const taskWrite = internalQuery({
  args: {
    changes: v.array(
      v.object({ id: v.id("tasks"), before: v.any(), after: v.any() }),
    ),
  },
  returns: readAccessResults,
  handler: async (ctx, { changes }) => {
    const identity = await ctx.auth.getUserIdentity();
    return await decideEach(changes, async ({ before, after }) => {
      if (identity === null) throw new Error("Unauthenticated");
      for (const row of [before, after]) {
        if (row === null) continue;
        const membership = await ctx.db
          .query("memberships")
          .withIndex("by_project_member", (q) =>
            q.eq("project", row.project).eq("member", identity.tokenIdentifier),
          )
          .unique();
        if (membership === null) throw new Error("Not a project member");
      }
      return null;
    });
  },
});
```

A write rule is not a mutation wrapper. It applies to every writer, public and
internal mutations alike, and to mutations called from other functions.

## What the rule receives

The rule receives `changes`, the net changes the transaction made to the
table, one per document:

| Field | Description |
| - | - |
| `id` | The document's ID |
| `before` | The document before the transaction, or `null` if it was inserted |
| `after` | The document at commit, or `null` if it was deleted |

A document inserted and then patched in the same transaction is one change
with `before: null`. The `TableChange` type from `bijection/server` describes
one change.

The rule runs on the committing transaction and sees its complete tentative
state. By default its reads see the database as it will be after the
transaction.

## What the rule returns

The rule returns one result per change, in order, and `[]` when there are no
changes. Each result is `null`, `{ validUntil }`, `{ reason }` or
`{ validUntil, reason }`, as for a [read rule](/access/read-rules#what-the-rule-returns).
Throw to refuse: a refusal fails the whole transaction, and nothing it wrote
commits.

A missing result, an extra one, or a bare value instead of an array refuses.
The rule's `returns` must be `readAccessResults`; a deployment whose write rule
declares anything else is refused. Every push of your code runs each write rule
once with no changes, which checks this before any real write depends on it.

When the rule reads the clock, each permission must carry `validUntil`, at
most 366 days ahead. The transaction must then commit before that instant.

## Private inputs and the original state

Pass an object instead of a bare reference to give the rule private inputs or
to choose which state it decides against:

```ts bijection/schema.ts {3-7} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
tasks: defineTable({ project: v.string(), title: v.string() })
  .index("by_project", ["project"])
  .govern({
    query: makeFunctionReference<"query">("access:taskWrite"),
    reads: ["memberships"],
    basis: "final",
  }),
```

* `reads` lists the tables the rule may read privately, at most 32 ordinary
  tables of your schema, exactly like a read rule's
  [policy inputs](/access/read-rules#policy-inputs). With `reads`, the rule may
  read those tables and nothing else.
* `basis` chooses the state the rule's reads see. `"final"`, the default, is
  the state at commit. `"original"` is the state before the transaction
  started.

Use `basis: "original"` on tables that hold authority, such as memberships or
grants. Deciding a new membership against the original state means the
authority to add it must already exist before the transaction: a caller can't
grant themselves a role and use it to approve their own grant in the same
commit.

## Errors

When a write rule refuses, the mutation fails with a `GoverningRuleRejected`
error. What the caller sees depends on whether the rule declares inputs:

| Rule | The caller sees | The rule's console output |
| - | - | - |
| A bare reference | `Governing query refused the change: <message>`, with the rule's message | Logged with the request |
| An object with `reads` | `Governing query refused the change: Read access was refused` | Discarded |

A rule with private inputs could mention them in its message, so Bijection
keeps its message private. To record why such a rule refused, audit the table
with `.access({ audit: true })`, which records the `why` of a thrown
`BijectionError({ kind: "AccessRefused", why })`. See
[Auditing decisions](/access/read-rules#auditing-decisions).

## Where write rules apply

* Only on tables your app owns. [Synced tables](/integrations/overview) hold
  a provider's data, and [views](/views/overview) are computed, so neither
  accepts `.govern`.
* A table has at most one write rule. To combine several conditions, check them
  all inside that rule.
* A write rule checks the changes of each transaction. It isn't a standing
  constraint: a rule on `tasks` that looks at a project doesn't run when the
  project changes.

## Options

| Option | Description |
| - | - |
| `query` | Required in the object form. The write rule, a query taking `changes` and returning `readAccessResults`. |
| `reads` | Required in the object form. The private input tables, at most 32. Use `[]` for a rule that reads nothing privately. |
| `basis` | `"final"` (default) or `"original"`: the state the rule's reads see. |
| `appliesDelegation` | States that the rule applies the narrowing claims of a delegated identity. A caller carrying such claims can't commit a change to a table whose write rule doesn't declare this, or to a table without one. |
