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

# First Protected Application

> Build a small application where people own the documents they create

This page builds a complete small application. A signed-in person creates
documents, lists them page by page, renames them and deletes them. Nobody else
can read or change those documents unless their owner shares them.

The functions are ordinary queries and mutations over `ctx.db`. Protection is
declared once, on the table, and Bijection enforces it on every read and write,
from any function.

## Choose a starter

`bijection access init` writes a starter policy into a project that has no
schema yet. Preview it first; the preview signs in to nothing and writes nothing:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection access init --template personal --dry-run
bijection access init --template personal
```

Each starter is ordinary TypeScript that you edit afterwards. None of them
grants a role at runtime by itself:

| Starter | Who can read | Who can write |
| - | - | - |
| `personal` | A document's owner, and people it is shared with | Any signed-in person creates; the owner edits, shares and deletes |
| `team` | Members of the team the document belongs to | `team:member` and `team:admin` on that team |
| `public-catalog` | Everyone, signed in or not | Holders of `root:editor`, granted explicitly |

The `team` starter declares each team as a tenant boundary and binds every
document to its team through a parent field and index. Membership is an
explicit grant on one team; signing in grants none.

Without `--dry-run`, setup reports each step separately:

1. **Source declarations written:** `access.ts`, `schema.ts` and `accessApi.ts`.
2. **Application deployed:** the deployment activated the authorization program.
3. **Application identity verified:** the deployment verified your application
   sign-in. A management login is never used as an application identity.
4. **Initial authority established:** the verified identity holds `root:owner`,
   the declared recovery role. Set `BIJECTION_INITIAL_OWNER`, or pass
   `--initial-owner`, to require a specific identity. The first caller never
   becomes an owner implicitly.
5. **Example check:** whether that owner may create a document on the root,
   decided by the installed policy. The check writes nothing; a refusal is
   reported rather than hidden.

A rerun resumes an interrupted setup. Once you edit the generated model, setup
keeps it and no starter replaces it.

## The access model

The `personal` starter writes this model:

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Application-owned authorization. Configure your identity provider separately.
import { defineAccess } from "bijection/server";

export const access = defineAccess({
  initialOwner: { environment: "BIJECTION_INITIAL_OWNER" },
  resources: {
    root: { recovery: { role: "root:owner" } },
    // Any signed-in person may create a document. Creating one assigns its
    // creator document:owner in the same transaction; nobody else receives a
    // role on it until someone with authority grants one.
    document: {
      permissions: ["read", "edit", "create", "delete"],
      audience: { create: "public" },
      create: { assign: { role: "document:owner", holder: "creator" } },
    },
  },
  roles: {
    "root:owner": ["root:*", "document:*"],
    "document:owner": ["document:*"],
    "document:editor": ["document:read", "document:edit"],
    "document:viewer": ["document:read"],
  },
});
```

* A **permission** names one operation on a resource type, such as
  `document:edit`.
* A **role** groups permissions. An **assignment** gives a holder a role on one
  resource.
* `audience: { create: "public" }` declares that every caller holds
  `document:create`. Creating a document also assigns `document:owner` to its
  creator, so a caller without an application identity cannot create one.

## The schema

The schema binds the `document` resource to a table. Every field names its
policy, through small constants you reuse:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
// Every field names its policy. A field without one fails type checking and
// activation, so adding a field is always a classification decision.
const editable = { read: "read", create: "create", update: "edit" } as const;
const fixed = { ...editable, update: false } as const;
const document = { read: "read", create: "create", delete: "delete" } as const;

export default defineSchema(
  {
    assignments: access.assignments(),
    // createdBy lets a person list what they created; it confers no authority.
    documents: defineTable({
      createdBy: v.string(),
      title: v.string(),
      body: v.string(),
    })
      .index("by_creator", ["createdBy"])
      .access(access.resources.document, {
        document,
        fields: { createdBy: fixed, title: editable, body: editable },
      }),
  },
  { access },
);
```

The file begins with imports of `defineSchema`, `defineTable`, `v` and the
`access` model. `assignments` stores the application's role assignments.

## Queries and mutations

```ts bijection/documents.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { paginationOptsValidator } from "bijection/server";
import { v } from "bijection/values";
import type { Id } from "./_generated/dataModel.js";
import {
  mutation,
  query,
  type MutationCtx,
  type QueryCtx,
} from "./_generated/server.js";

async function signedIn(ctx: QueryCtx | MutationCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (identity === null) throw new Error("Sign in first");
  return identity.tokenIdentifier;
}

export const create = mutation({
  args: { title: v.string(), body: v.string() },
  handler: async (ctx, { title, body }) =>
    ctx.db.insert("documents", {
      createdBy: await signedIn(ctx),
      title,
      body,
    }),
});

export const get = query({
  args: { documentId: v.id("documents") },
  handler: (ctx, { documentId }) => ctx.db.get("documents", documentId),
});

// Complete: every document in the range must be readable, or the read fails.
export const mine = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, { paginationOpts }) => {
    const createdBy = await signedIn(ctx);
    return ctx.db
      .query("documents")
      .withIndex("by_creator", (q) => q.eq("createdBy", createdBy))
      .order("desc")
      .select(["_id", "title"])
      .paginate(paginationOpts);
  },
});

// Visible: unreadable documents are skipped before pagination.
export const mineVisible = query({
  args: { paginationOpts: paginationOptsValidator },
  handler: async (ctx, { paginationOpts }) => {
    const createdBy = await signedIn(ctx);
    return ctx.db
      .query("documents")
      .visible()
      .withIndex("by_creator", (q) => q.eq("createdBy", createdBy))
      .order("desc")
      .select(["_id", "title"])
      .paginate(paginationOpts);
  },
});

async function renameDocument(
  ctx: MutationCtx,
  documentId: Id<"documents">,
  title: string,
) {
  await ctx.db.patch("documents", documentId, { title: title.trim() });
}

export const rename = mutation({
  args: { documentId: v.id("documents"), title: v.string() },
  handler: (ctx, { documentId, title }) =>
    renameDocument(ctx, documentId, title),
});

// A document's assignments go first; the delete itself needs document:delete.
export const remove = mutation({
  args: { documentId: v.id("documents") },
  handler: async (ctx, { documentId }) => {
    await ctx.access.revokeAll({ type: "document", id: documentId });
    await ctx.db.delete("documents", documentId);
  },
});
```

None of these handlers checks a permission. The table policy decides:

* `create` needs `document:create`, which the model makes public. The server
  sets `createdBy`; a caller cannot choose it.
* `mine` is an ordinary query and returns the **complete** requested range: if
  any document in it is not readable, for example after its ownership was
  revoked, the query fails rather than returning a shorter page.
* `mineVisible` adds `.visible()` and returns only the documents the caller can
  read, chosen before pagination. Use it when the feature is to discover
  readable documents.
* `rename` calls `renameDocument`, a plain TypeScript helper. It needs no
  registration, runs in the same transaction, and its `ctx.db.patch` is checked
  against `document:edit` at commit, as it would be from any public or internal
  mutation.
* `remove` revokes the document's assignments, then deletes it with
  `document:delete`.

`select(["_id", "title"])` returns only those fields, and the result type says
so.

## Call it from React

Generated references keep argument and result types:

```tsx theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { useMutation, usePaginatedQuery } from "bijection/react";
import { api } from "../bijection/_generated/api";

const { results, status, loadMore } = usePaginatedQuery(
  api.documents.mine,
  {},
  { initialNumItems: 20 },
);
const rename = useMutation(api.documents.rename);
```

`results[0]?.title` is a `string`; `results[0]?.body` is a type error because
the query does not select it.

## Share a document

The owner shares a document by granting a role on it. From the command line,
signed in as the owner:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection access grant document:viewer --to identity:<issuer|subject> --on document/<id>
```

The viewer can now `get` the document; `rename` stays refused. It does not
appear in the viewer's `mine` lists, which select by creator. Listing documents
shared with you uses a declared access path; see
[read rules](https://docs.bijection.com/access/read-rules.md). The generated `accessApi.grant` and
`accessApi.revoke` functions offer the same operations to the application.
Revoking the assignment affects the viewer's next reads and live subscriptions.

## Add a sensitive field

Adding a field is a classification decision. Suppose owners keep private notes
that shared viewers must not read. Declare a permission for them in the model:

```ts bijection/access.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
document: {
  permissions: ["read", "read.private", "edit", "create", "delete"],
  // audience and create stay as they were
},
```

Then classify the field with its own policy:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
const sensitive = { ...editable, read: "read.private" } as const;

documents: defineTable({
  createdBy: v.string(),
  title: v.string(),
  body: v.string(),
  privateNotes: v.optional(v.string()),
})
  .index("by_creator", ["createdBy"])
  .access(access.resources.document, {
    document,
    fields: {
      createdBy: fixed,
      title: editable,
      body: editable,
      privateNotes: sensitive,
    },
  }),
```

Without the `privateNotes` entry, the table fails type checking and activation
refuses it. `document:owner` holds `document:*`, so owners read the notes;
`document:viewer` and `document:editor` do not. Reads that viewers make should
select the fields they may read, as `mine` does.

## Who can do what

| Action | Owner (creator) | Viewer | Editor | Anyone else | Initial owner |
| - | - | - | - | - | - |
| Create a document | Yes | Yes | Yes | If signed in | Yes |
| Read it | Yes | Yes | Yes | No | Yes |
| Edit `title` and `body` | Yes | No | Yes | No | Yes |
| Change `createdBy` | No | No | No | No | No |
| Share, revoke and delete | Yes | No | No | No | Yes |

The initial owner holds `root:owner`, which covers every document. Grant it
only to the application's administrators.

## Next steps

* [Authorization overview](https://docs.bijection.com/access/overview) explains the model.
* [Read rules](https://docs.bijection.com/access/read-rules.md) covers complete and visible reads, exact
  selections and indexes.
* [Write rules](https://docs.bijection.com/access/write-rules.md) covers creation, field updates and
  internal mutations.
* [Access model](https://docs.bijection.com/access/access-model.md#tenant-boundaries) covers tenant
  boundaries, which the `team` starter uses.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.