> ## 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.

# Access Model

> Declare types, permissions, roles and grants once and generate your read, disclosure and write rules

<Warning>
  The access model is in beta.
</Warning>

Hand-written [read](/access/read-rules) and [write rules](/access/write-rules)
work well for one or two tables. Once you have organizations, projects inside
them, tasks inside projects, and people who hold different roles at each
level, the same checks repeat in every rule.

`defineAccessModel` from `bijection/server` lets you declare that structure
once. It generates the bodies of your read, disclosure and write rules and
gives your functions helpers to check permissions and manage grants. It is a
library that runs inside your own rules, not a separate service: the rules are
still queries you export, and Bijection enforces them as it does any other
rule.

## Concepts

The model has five parts.

* **Types.** Everything you protect is an object of a type. A type is either
  backed by a table, where each row is an object, or *key-only*, such as an
  organization or a group, whose objects exist only as keys in grants. Types
  form a tree through a `parent` field on each row. A type without a parent
  hangs from the built-in `root` object, whose key is `*`.
* **Permissions.** A permission is one capability on one type, written
  `type:name`, such as `task:read` or `project:grant.viewer`. Your code checks
  permissions, never roles.
* **Roles.** A role is a named bundle of permissions on its own type and on
  descendant types. `project:owner` including `task:*` means a project's owner
  can do anything to its tasks.
* **Grants.** A grant says that a user, or the members of a group, hold a role
  on one object. Grants are rows of a grants table. A row field can also
  confer a role, such as a task's `assignee` field conferring `task:assignee`
  on the user it names.
* **Restrictions.** A restriction makes an object require its caller to pass
  another object, whatever roles they hold. The common case is a tenant: every
  project belongs to an organization, and only people who pass that
  organization can reach its projects.

Users are identified by their `tokenIdentifier` (see
[Auth in Functions](/auth/functions-auth#user-identity-fields)). Grants and
row fields that name users store that string.

## Declaring a model

```ts bijection/accessModel.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineAccessModel } from "bijection/server";

export const access = defineAccessModel({
  categories: { tenant: "any" },
  types: {
    org: { permissions: ["pass", "administer"] },
    project: {
      table: "projects",
      parent: { field: "org", type: "org" },
      restrictions: { root: { type: "org", category: "tenant" } },
      read: { discover: ["_id", "_creationTime", "org", "name"], read: "rest" },
      rowGrants: { createdBy: "project:owner" },
    },
    task: {
      table: "tasks",
      parent: { field: "project", type: "project" },
      write: { edit: "rest", "edit.status": ["status"] },
      rowGrants: { assignee: "task:assignee" },
    },
  },
  roles: {
    "org:admin": ["org:*", "project:*", "task:*"],
    "org:member": ["org:pass", "project:discover", "project:create"],
    "project:owner": ["project:*", "task:*"],
    "project:viewer": ["project:discover", "project:read", "task:read"],
    "task:assignee": ["task:read", "task:edit.status"],
  },
});
```

`defineAccessModel` checks the declaration and throws if it is inconsistent,
for example if a role names an undeclared permission, if the parents don't
form a tree, or if a role grants a read permission without the permission that
makes the object visible.

### Types

| Field | Description |
| - | - |
| `table` | The table backing the type. Omit it for a key-only type. |
| `parent` | `{ field, type, index? }`: the row field holding the parent's key, the parent's type, and the index pinning the field (default `by_<field>`). Key-only types can't have a parent. |
| `read` | Read permissions by the fields they cover. Every field belongs to exactly one; `"rest"` covers the fields no other permission lists. The permission covering `_id` decides whether the object is visible at all. Default `{ read: "rest" }`. |
| `write` | Write permissions by the fields they cover. Default `{ edit: "rest" }`. |
| `permissions` | Additional permission names. `create`, `delete` and one `grant.<role>` per role of the type are always declared. |
| `rowGrants` | Row fields conferring a role on the users they name, keyed by field name. |
| `restrictions` | `root: { type, category }`: the tenant the caller must pass. `field: { name, type }`: a row field listing restricting objects (markings). Descendants inherit both. |
| `category` | For a restricting type, the category of its objects: a fixed name, or `{ from: "key" }` for the prefix of the key before `:`. |
| `holders` | A permission of the type whose holders may see who holds grants on an object. Without it, only holders of a `grant.*` permission may. |

### Roles

A role is a list of permissions, written `type:name`, `type:*` or
`type:grant.*`. It can also be an object:

| Field | Description |
| - | - |
| `permissions` | The permissions the role bundles. |
| `maxDuration` | Grants of the role must expire within this many milliseconds, at most 366 days. |
| `justification` | Every grant of the role needs a written `reason`. |
| `selfGrant` | A permission of the type whose holders may grant the role to themselves. Such a role must also declare `maxDuration` and `justification`. |

### Restrictions and categories

`categories` names each restriction category and how it combines. In an `"all"`
category a caller must pass every restricting object of that category an
object carries; in an `"any"` category, at least one. Passing an object means
holding its `pass` permission.

A tenant is a restriction in an `"any"` category, as `tenant` above. Markings,
such as `data:pii`, are key-only types listed in a row's restriction field;
adding a marking needs `apply` on it and removing one needs `declassify`.

## Wiring the rules

The model supplies rule bodies. Export each one as a query, one set per type
and one for the grants table:

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

const decides = { args: readAccessArgs, returns: readAccessResults };
const releases = { args: disclosureArgs, returns: readAccessResults };
const governs = { args: { changes: v.array(v.any()) }, returns: readAccessResults };

export const projectRead = internalQuery({ ...decides, handler: access.read("project") });
export const projectDisclose = internalQuery({ ...releases, handler: access.disclose("project") });
export const projectWrite = internalQuery({ ...governs, handler: access.write("project") });

export const taskRead = internalQuery({ ...decides, handler: access.read("task") });
export const taskDisclose = internalQuery({ ...releases, handler: access.disclose("task") });
export const taskWrite = internalQuery({ ...governs, handler: access.write("task") });

export const grantsRead = internalQuery({ ...decides, handler: access.grantsRead });
export const grantsDisclose = internalQuery({ ...releases, handler: access.grantsDisclose });
export const grantsWrite = internalQuery({ ...governs, handler: access.grantsWrite });

// Deletes a grant once it has expired; see Granting and revoking.
export const expireGrant = internalMutation({
  args: { id: v.id("grants") },
  handler: access.expireGrant,
});
```

Then attach them in your schema. `access.grantsTable()` defines the grants
table with its indexes, and `access.inputs()` lists the policy inputs every
rule of the model reads:

```ts bijection/schema.ts {9-12,23-28} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineSchema, defineTable, makeFunctionReference } from "bijection/server";
import { v } from "bijection/values";
import { access } from "./accessModel";

const rule = (name: string) => makeFunctionReference<"query">(`access:${name}`);
const reads = access.inputs();

export default defineSchema({
  grants: access
    .grantsTable()
    .access({ read: rule("grantsRead"), disclose: rule("grantsDisclose"), reads })
    .govern({ query: rule("grantsWrite"), reads, basis: "original" }),
  projects: defineTable({
    org: v.string(),
    name: v.string(),
    createdBy: v.optional(v.string()),
  })
    .index("by_org", ["org"])
    .index("by_createdBy", ["createdBy"])
    .access({ read: rule("projectRead"), disclose: rule("projectDisclose"), reads })
    .govern({ query: rule("projectWrite"), reads }),
  tasks: defineTable({
    project: v.string(),
    title: v.string(),
    status: v.string(),
    assignee: v.optional(v.string()),
  })
    .index("by_project", ["project"])
    .index("by_assignee", ["assignee"])
    .access({ read: rule("taskRead"), disclose: rule("taskDisclose"), reads })
    .govern({ query: rule("taskWrite"), reads }),
});
```

The grants table's write rule uses `basis: "original"`, so the authority to
grant a role must exist before the transaction that grants it.

Parent fields hold the parent's key: an ID for a table-backed parent, any
string for a key-only one. Each table needs the index its `parent` names, and
each scalar row-grant field a `by_<field>` index for `access.visible`. The
model doesn't see your schema, so a missing index fails when a query needs it.

<Tip>
  A generated read rule decides a range pinned on the parent field the same way
  for every row. Add `rowsFollowRange: access.rowsFollowRange("task")` to the
  table's `.access` to let large ranges be decided once. See
  [Large ranges](/access/read-rules#large-ranges).
</Tip>

## What the generated rules decide

**Reads.**

| Read | Permitted when |
| - | - |
| One object by ID | The caller passes its restrictions and holds the permissions for the properties read |
| A range pinned on the parent field | The caller passes the parent's restrictions and holds the permissions on the parent or above |
| A range pinned on a row-grant field naming the caller | The role that field confers includes the permissions; each returned row is still decided alone |
| Any other range | The caller holds the permissions on the `root` object |

A read of the whole document needs every read permission of the type. If a
type puts a field in a read permission of its own, for example
`"read.finance": ["budget"]`, a caller without it can read the other fields
only by [selecting](/access/read-rules#reading-fewer-properties) them.

**Writes.**

| Change | Requires |
| - | - |
| Insert | `create` on the parent. A row-grant field must name only the caller or be covered by the role's `grant.*` permission. Each marking added needs `apply`. |
| Update | For each changed field, the write permission covering it, checked against the document before the change |
| Changed parent field | `delete` where the row was and `create` on the new parent; restrictions the row carried must still apply or be declassified |
| Changed row-grant field | The matching `grant.*` permission |
| Changed restriction field | `apply` on each marking added and `declassify` on each one removed |
| Delete | `delete` on the object, with no grants left on it and no child rows under it |

**Grants.** Inserting a grant of role R on an object requires `grant.R` on it
and respects the role's `maxDuration` and `justification`. Grants are never
edited: a change is a revocation plus a new grant. Revoking a grant needs the
same `grant.R` permission, except that users may always remove their own grant
and anyone may remove a grant that has expired. The last administrator of a
top-level object can't be removed.

## Checking permissions in functions

The rules enforce access. In your functions, the model's helpers answer
questions about it, for example to show or hide a button:

| Helper | Returns |
| - | - |
| `access.can(ctx, permission, type, key)` | `null` or `{ validUntil }` when permitted; throws a `BijectionError` of kind `AccessRefused` otherwise |
| `access.audited(ctx, permission, type, key)` | The same as `can`, and records the decision in the audit log when the model declares `audit` |
| `access.permissionsOn(ctx, type, key)` | The caller's effective permissions on the object, including `create` of child types |
| `access.visible(ctx, type, { permission, limit })` | Keys of objects of the type the caller reaches; throws rather than truncating past `limit` (default 100) |
| `access.holders(ctx, type, key)` | The grants and row fields conferring roles on the object, visible to holders of a `grant.*` permission on it or of the type's `holders` permission |
| `access.explain(ctx, permission, type, key)` | `{ allowed, path?, refusedBy? }`: how access is reached, or what refuses it |

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

export const renameProject = mutation({
  args: { id: v.id("projects"), name: v.string() },
  handler: async (ctx, { id, name }) => {
    // A clear error up front. The write rule enforces it regardless.
    await access.can(ctx, "project:edit", "project", id);
    await ctx.db.patch("projects", id, { name });
  },
});
```

The helpers run as ordinary reads by the caller, so they only see what the
caller may read. If a helper needs a row the caller can't read, the calling
function is refused rather than given a guess. Their answers are advisory; the
rules decide.

## Granting and revoking

Grant a role with `access.grant` inside a mutation. The grants table's write
rule checks the caller's authority when the mutation commits:

```ts bijection/sharing.ts {9-18} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { v } from "bijection/values";
import { internal } from "./_generated/api";
import { mutation } from "./_generated/server";
import { access } from "./accessModel";

export const shareProject = mutation({
  args: { project: v.id("projects"), user: v.string() },
  handler: async (ctx, args) => {
    await access.grant(
      ctx,
      {
        type: "project",
        object: args.project,
        role: "project:viewer",
        subject: { user: args.user },
        expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
      },
      { expire: internal.access.expireGrant },
    );
  },
});
```

A grant to the members of a group uses
`subject: { type: "group", key, permission: "member" }`, with the group type
listed in the model's `groups: { types: ["group"] }`.

A grant with `expiresAt` needs the `expire` option: `access.grant` schedules
your `expireGrant` mutation at that time, so the grant's deletion is an
ordinary commit and live queries lose access when it happens. `reason` records
why the grant was made.

To delete an object, first revoke every grant on it with
`access.revokeAll(ctx, type, key)`; the grants write rule decides each
revocation, so the caller needs the authority to revoke them.

### The first administrator

In a fresh deployment nobody holds any role, so nobody could grant one. While
the grants table and every table of the model are empty, a signed-in caller may
insert exactly one grant: an administering role of a top-level type, such as
`org:admin`, to themselves.

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// In a mutation, as the signed-in user
const identity = await ctx.auth.getUserIdentity();
await access.grant(ctx, {
  type: "org",
  object: "acme",
  role: "org:admin",
  subject: { user: identity!.tokenIdentifier },
});
```

## Model options

| Option | Description |
| - | - |
| `types` | Required. The types, by name. |
| `roles` | Required. The roles, by `type:name`. |
| `grants` | The grants table's name. Default `grants`. |
| `categories` | Restriction categories, each `"all"` or `"any"`. |
| `groups` | `{ types, depth }`: types whose members can hold grants, and how deeply groups nest (1 to 3). |
| `users` | `{ table, index, user }`: resolve callers to a local user key through an identity table keyed by `tokenIdentifier`, instead of using `tokenIdentifier` itself. |
| `audit` | Record decisions made through `access.audited`, and grant changes made through `access.grant`, `access.expireGrant` and `access.revokeAll`, in the audit log. |
