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

# Read Rules

> Decide which rows and properties of a table each caller can read

<Warning>
  Read rules are in beta.
</Warning>

A read rule decides every read of a table: a `get` by ID, an index range, a
search, a count. You declare it on the table with `.access` and write it as an
ordinary query that answers the reads Bijection asks it about.

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

export default defineSchema({
  memberships: defineTable({ project: v.string(), member: v.string() })
    .index("by_project_member", ["project", "member"]),
  tasks: defineTable({ project: v.string(), title: v.string() })
    .index("by_project", ["project"])
    .access({
      read: makeFunctionReference<"query">("access:taskRead"),
      reads: ["memberships", "tasks"],
    }),
});
```

The [overview](/access/overview) shows the matching `taskRead` rule. The rest
of this page describes what a rule receives, what it returns, and what callers
see when it refuses.

## Declaring a read rule

`.access` takes an object with two required fields:

* `read`: a reference to the rule, a query exported from a module of the same
  [component](/components/overview). It can be public or internal; an
  [internal query](/functions/internal-functions) keeps clients from calling it
  directly.
* `reads`: the tables the rule may read, its *policy inputs*. List every table
  the rule queries, including the protected table itself if the rule looks up
  the row it's deciding.

`.access` can be declared on a table or on a [view](/views/overview). A table
has at most one read rule; several tables can share the same rule.

<Tip>
  Referencing the rule with `makeFunctionReference<"query">("module:export")`
  keeps your schema from importing `./_generated/api`, whose types depend on
  the schema itself.
</Tip>

When you push your code, Bijection checks each rule. A deployment is refused
when the rule isn't an exported query, when its `args` aren't
`readAccessArgs`, or when its `returns` isn't `readAccessResults`.

## What the rule receives

A rule receives a batch of requests: every read of the protected table that
the calling function made and that has to be decided together.

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
{
  requests: Array<{
    table: string;
    properties: string[] | null;
    target:
      | { kind: "object"; id: string }
      | {
          kind: "query";
          equalities: Array<{ field: string; value?: Value }>;
          covers?: true;
        };
  }>;
}
```

* `table` is the table being read.
* `target` is what is being read:
  * `object` is one document, named by its ID, as `ctx.db.get` reads it.
  * `query` is a range or scan. `equalities` lists the fields the query's index
    range pins with `.eq(...)`. They are guaranteed index bounds, not a guess
    from `.filter()`. An equality without a `value` means the field is missing;
    `null` is an explicit value. A query with no equalities is a read of the
    whole table.
* `properties` is the list of properties the read uses, including those that
  decide membership, filtering and order, and `_id` and `_creationTime`.
  `null` means the whole document.

Every row a query returns also arrives as its own `object` request, so a rule
can decide a range and still look at each row.

## What the rule returns

A rule returns one result per request, in the same order:

| Result | Meaning |
| - | - |
| `null` | Permitted |
| `{ validUntil }` | Permitted until `validUntil`, in epoch milliseconds |
| `{ reason }` | Permitted, with a reason recorded in the audit log |
| `{ validUntil, reason }` | Both |

To refuse, throw. A throw refuses the whole batch.

Bijection refuses the batch when a result is missing, when there are too many,
or when one isn't a permission. A rule that returns early or falls off its end
therefore refuses instead of permitting reads it never looked at.

`decideEach(requests, decide)` calls `decide` for each request in order and
collects the results, which is the easiest way to get this right. A rule that
decides the whole table at once, for example "administrators read everything",
still answers each request:

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
if (await isAdministrator(ctx)) {
  return requests.map(() => null);
}
```

### Time-limited permissions

A rule that reads the clock must say how long its answer holds. Return
`{ validUntil }` for each permitted request; a rule that observes the time and
returns `null` is refused. `validUntil` must be in the future and at most 366
days ahead.

### Reasons

A reason is a string of at most 256 bytes. It never reaches the caller: it is
written to the audit log when the table is [audited](#auditing-decisions). To
give the reason for a refusal, throw a `BijectionError`:

```ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { BijectionError } from "bijection/values";

throw new BijectionError({ kind: "AccessRefused", why: "not a member" });
```

## Policy inputs

While the rule runs, it can read the tables listed in `reads` and nothing
else. Reading any other table fails with
`Policy input is outside its admitted scope`. The list holds at most 32 tables,
and each must be an ordinary table of your schema, not a view.

Policy inputs are private to the rule. The caller doesn't need permission to
read them, and nothing the rule computes from them reaches the caller: a
refused read reports `Read access was refused`, whatever the rule threw, and
the rule's console output is not shown to the caller.

<Warning>
  Listing a table in `reads` makes it readable by the rule, not private
  everywhere. A membership table without rules of its own can still be read and
  written by any of your functions. Give it its own read and
  [write rules](/access/write-rules), or only touch it from internal functions.
</Warning>

## What callers see

A read rule never changes the data it permits. Refused reads behave as
follows:

* **An index range in a query** leaves out the rows the rule refuses and
  returns the rest, as if the refused rows didn't exist. The rule still has to
  permit the range itself: if it refuses the range's `query` request, the whole
  query is refused.
* **Every other refused read** refuses the whole function with
  `Read access was refused`. That includes `ctx.db.get`, reads of a whole table,
  searches and counts, and every read inside a mutation.
* **A refusal never redacts.** If a rule refuses a property, the read of the
  document that includes it is refused; Bijection doesn't return the document
  with the property removed.

A rule that refuses an ID it can't find, as `taskRead` does, answers a hidden,
a deleted and a never-existing document the same way, so a refusal doesn't
reveal whether an ID exists.

### Reading several documents by ID

`getMany` from `bijection/server` reads up to 100 documents of one table and
answers each ID on its own. In a query, a refused document answers
`restricted` while the others are returned:

```ts bijection/tasks.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { getMany } from "bijection/server";
import { v } from "bijection/values";
import { query } from "./_generated/server";

export const tasksById = query({
  args: { ids: v.array(v.string()) },
  handler: async (ctx, { ids }) => {
    const answers = await getMany(ctx, "tasks", ids);
    // Each answer is { id, kind: "object", object }, { id, kind: "absent" }
    // or { id, kind: "restricted" }.
    return answers;
  },
});
```

`absent` is only reported to a caller the rule permits to read the whole
table; for anyone else a missing ID is `restricted`. In a mutation, `getMany`
behaves like a series of `ctx.db.get` calls, and one refusal refuses the whole
mutation.

### Reading fewer properties

When a caller may read only some properties, ask for only those with
`.select`. The rule then receives just the selected properties plus the ones
the query uses to find and order rows:

```ts bijection/customers.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const names = await ctx.db
  .query("customers")
  .withIndex("by_region", (q) => q.eq("region", region))
  .select(["name"])
  .collect();
```

## Reactivity

The reads a rule makes are dependencies of the query it decided. When a
membership row changes, Bijection reruns the subscriptions whose decision read
it: a live query that was refused becomes permitted after a grant, and a
permitted one is refused after a revocation.

A refusal carries no expiry of its own. If access should begin at a later time,
make that a committed change, for example with a
[scheduled function](/scheduling/scheduled-functions) that inserts the
membership.

<Note>
  Scheduled functions and cron jobs run without a user identity, so inside a
  rule `ctx.auth.getUserIdentity()` returns `null` for their reads.
</Note>

## Large ranges

Each function can carry at most 128 decided reads of protected tables, and
every returned row counts as one. A function that reads more rows than that
from protected tables fails with `ReadAccessBounds`.

When your rule decides a whole range the same way for every row in it, declare
that with `rowsFollowRange`. When deciding the range's rows one by one would
exceed the bound, Bijection keeps one decision for the range instead:

```ts bijection/schema.ts {6} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
tasks: defineTable({ project: v.string(), title: v.string() })
  .index("by_project", ["project"])
  .access({
    read: makeFunctionReference<"query">("access:taskRead"),
    reads: ["memberships", "tasks"],
    rowsFollowRange: [{ index: "by_project", field: "project" }],
  }),
```

Each entry names an index of the table whose first field is `field`. The range
must pin `field` with an equality. The rule then receives a `query` request
with `covers: true`, and its answer decides every row the range covers. A text
or vector search whose filter pins the same field searches that range's rows
only.

If rows in the range carry their own labels that the rule checks, name that
field as `markings`. Bijection then keeps one decision per distinct value of
that field within the range.

`rowsFollowRange` is a promise you make about your rule. In tests and
development builds Bijection checks it: when the rule permits a covering
request, every row it covers is decided too, and a row the rule would refuse
fails the function with `RowsFollowRangeViolated`. A release build trusts the
declaration unless `BIJECTION_CHECK_DECLARED_RANGES` is set.

## Auditing decisions

Add `audit: true` to record every decision of the table's rules in the
deployment's [audit log](/production/integrations/audit-logging): each read
its read rule decides, each release its [disclosure rule](/access/disclosure)
decides and each change its [write rule](/access/write-rules) decides.

```ts bijection/schema.ts {4} theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
.access({
  read: makeFunctionReference<"query">("access:taskRead"),
  reads: ["memberships", "tasks"],
  audit: true,
}),
```

A line names the caller, the rule, what it decided, whether it permitted and
the reason the rule gave. A refused decision's line names no document ID and
no value. Bijection writes the lines itself, so the function can't skip them.

## Options

| Option | Description |
| - | - |
| `read` | Required. The read rule, a query taking `readAccessArgs` and returning `readAccessResults`. |
| `reads` | Required. The policy input tables, at most 32. Use `[]` for a rule that reads nothing. |
| `disclose` | The [disclosure rule](/access/disclosure). |
| `rowsFollowRange` | Up to 16 ranges whose decision decides every row they deliver. See [Large ranges](#large-ranges). |
| `audit` | Record every decision of the table's rules. See [Auditing decisions](#auditing-decisions). |
| `appliesDelegation` | States that the rule applies the narrowing claims of a delegated identity. A caller carrying such claims is refused on every table whose read rule doesn't declare this, including tables without a read rule. |
