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

# Disclosure Rules

> Decide where data read from a protected table may be copied

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

A [read rule](/access/read-rules) decides who may see a table's data. Once a
function has read protected rows, though, anything it writes or sends may
carry that data somewhere else: into another table, an HTTP request or a
command to an external system. Bijection treats each such copy as a
*release* and asks the source table's disclosure rule whether it is allowed.

You declare the disclosure rule next to the read rule:

```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"])
  .access({
    read: makeFunctionReference<"query">("access:taskRead"),
    disclose: makeFunctionReference<"query">("access:taskDisclose"),
    reads: ["memberships", "tasks"],
  }),
```

<Warning>
  Without a `disclose` rule, a transaction that read protected rows of the
  table can't write to any table, including the same one, and an action that
  read them can't send them in an HTTP request. Reading stays unaffected.
</Warning>

## Writing a disclosure rule

A disclosure rule is a query that takes `disclosureArgs` and returns
`readAccessResults`, like a read rule. It uses the same policy inputs, the
tables listed in `reads`.

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

// Task data may be copied into tasks and taskHistory, by project members.
export const taskDisclose = internalQuery({
  args: disclosureArgs,
  returns: readAccessResults,
  handler: async (ctx, { requests, destination }) => {
    if (
      destination.kind !== "table" ||
      destination.component !== null ||
      !["tasks", "taskHistory"].includes(destination.table)
    ) {
      throw new Error("Task data can't be released there");
    }
    const identity = await ctx.auth.getUserIdentity();
    if (identity === null) throw new Error("Unauthenticated");
    return await decideEach(requests, async (request) => {
      // Decide each request as the read rule does.
      // ...
      return null;
    });
  },
});
```

The rule receives:

* `requests`: the reads of this table that the releasing function made, in the
  same shape a [read rule](/access/read-rules#what-the-rule-receives) receives.
* `destination`: where the data is going.

It returns one result per request, and throws to refuse the release.

## Destinations

`destination.kind` says what kind of release is being decided:

| Kind | Fields | Decided |
| - | - | - |
| `table` | `component`, `table`, `changes` (`id`, `before`, `after`) | When a transaction that read the table writes. Checked against access before and after the change; a concurrent revocation conflicts with the commit. |
| `http` | `url`, `method` | Just before an action sends an HTTP request after reading the table. Each redirect is checked separately. |
| `command` | `component`, `integration`, `command`, `target`, `contract`, `phase`, `args` | Before an [integration](/integrations/overview) command carrying the data is accepted, sent or reconciled. |
| `custody` | `component` | When a transaction that read the table schedules or cancels work of a component. See [Scheduled work](#scheduled-work). |

`component` is `null` for your app's own tables and the component path
otherwise. For a `table` release, `changes` holds each written document before
and after the write, `null` for absent, so the rule can check what is being
written as well as where.

## What a release means

The evidence for a release is every protected read the transaction made, not
the individual fields that ended up in the output. A mutation that reads a
task and then writes an unrelated row still releases task data into that row's
table.

Once a table release commits, the new rows are governed by their own table's
rules. Revoking access to the source afterwards doesn't retract what was
already written. A [view](/views/overview), in contrast, keeps the obligations
of the rows it was computed from.

An HTTP release authorizes the destination, not the body: Bijection doesn't
inspect what the request contains. Once an external system has received data,
Bijection can't recall it.

## Scheduled work

A `custody` release is decided when a transaction that read the table
schedules or cancels work of a component, when a scheduled action starts, and
for reads the scheduled work makes without a caller. It is always decided with
no identity: `ctx.auth.getUserIdentity()` returns `null`.
A rule that only permits signed-in members therefore never permits it.

Custody releases nothing by itself. What the scheduled work keeps still carries
the source's read obligations, and each later write, request or effect it makes
is checked as a release of its own. To let your own scheduled functions work
with task data, permit custody for your app's root component:

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
if (destination.kind === "custody" && destination.component === null) {
  return requests.map(() => null);
}
```

<Note>
  Permitting custody lets deployment administrators who can't read the table
  see that the work exists, its target function, its timing and argument size,
  and the outcome and resource usage of each attempt. Its arguments, logs and
  return value stay withheld.
</Note>
