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

# Walkthrough: From a Source System to an Operation

> Connect an external system, relate its records to your own, define a business object, protect it, change it through an operation and follow the outcome

This walkthrough builds one small, complete slice of a Bijection app. A CRM
holds customer accounts. Your app keeps its own customers, relates CRM accounts
to them, shows each customer as one business object, lets permitted staff rename
an account, and follows that rename until the CRM confirms it.

Along the way you meet each idea that sets Bijection apart from an ordinary app
backend:

| Step | You declare | Bijection enforces |
| - | - | - |
| [1. Describe the source](#1-describe-the-source) | The CRM's requests, responses and commands | Every record it publishes is backed by a retained provider response |
| [2. Declare who owns which facts](#2-declare-who-owns-which-facts) | A synced table, a local table and a link type | Only the sync writes CRM data; link cardinality holds on every write |
| [3. Define the business object](#3-define-the-business-object) | A keyed `Customer` view | One meaning, read like a table, reactive in queries |
| [4. Protect it](#4-protect-it) | Read and disclosure rules | Every read and every copy is decided, whichever function makes it |
| [5. Connect the account](#5-connect-the-account) | A connection per deployment | Credentials never reach your code |
| [6. Change it with an operation](#6-change-it-through-an-operation) | `defineOperation` with an external call | Local change and external intent commit together |
| [7. Follow the outcome](#7-follow-the-outcome) | Nothing new | Local acceptance, provider confirmation and publication stay distinct |

The code uses only the functions you already know: queries, mutations and
`ctx.db`. What changes is that ownership, relationships, rules and external
effects are declared once and enforced by the backend.

<Note>
  The walkthrough assumes you have [installed the CLI](/get-started/install)
  and have a project running with `bijection dev`. The CRM in it is
  illustrative: point the connection at a provider whose API actually offers the
  guarantees you declare.
</Note>

## 1. Describe the source

An [integration](/integrations/overview) states, in TypeScript, what Bijection
needs to know about one external system: the requests it may make, what each
response proves, which collections it reads and which commands it may send back.

```ts bijection/crm.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineIntegration, makeFunctionReference } from "bijection/server";
import { v } from "bijection/values";

export const crm = defineIntegration({
  http: {
    // Request contracts: `identity`, `changes`, `renameSend`, `renameProbe`.
    …
  },
  async identify(ctx) { … },
  collections: {
    accounts: {
      schema: { external_id: v.string(), name: v.string() },
      protocol: {
        kind: "change_feed",
        position: { domain: "account_changes", encoding: "uint64_decimal" },
        checkpoint: {
          initial: "0",
          continuity: "complete_prefix",
          retention: { kind: "stated", unit: "days", value: 90 },
        },
        records: "full_or_delete",
        atomic_group: "one_change",
      },
      sync: { every: { minutes: 5 }, async read(ctx, input) { … } },
    },
  },
  commands: {
    rename: {
      target: "accounts",
      args: { name: v.string() },
      governingRule: makeFunctionReference<"query", any, [null]>(
        "access:renameRule",
      ),
      delivery: {
        kind: "idempotent",
        retention: { kind: "stated", unit: "days", value: 90 },
        max_delivery_hours: 1,
        request_expiry: "provider_enforced",
      },
      condition: {
        kind: "target_version",
        from: { kind: "record_observation_position" },
      },
      async send(ctx) { … },
      async reconcile(ctx) { … },
    },
  },
});
```

[Defining integrations](/integrations/defining-integrations) shows the complete
request contracts and handlers, including a
[command](/integrations/defining-integrations#commands). Three things matter
here:

* **The protocol states what the provider guarantees.** This CRM offers an
  ordered change feed that it retains for 90 days. Bijection decides what a sync
  may publish from that declaration, so it has to match the provider's
  documentation. A plain paged listing declares a weaker protocol, and
  declaring more cannot make the provider offer it.
* **The command states how a change is delivered and proved.** `idempotent`
  says the CRM deduplicates requests by their identity for 90 days, so a lost
  response can be retried under the same identity. `condition` says the CRM
  checks the record version the request was based on. `reconcile` is how
  Bijection later asks the CRM what became of a request. `governingRule` names
  the query that decides who may have the CRM make this change; every command
  declares one, and you write it in [step 4](#4-protect-it).
* **No address or credential appears.** They belong to the connection you
  create in [step 5](#5-connect-the-account), so the same definition runs
  against a test account and a production account.

## 2. Declare who owns which facts

A Bijection schema says who is allowed to write each table. Three kinds of
tables appear here:

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

export default defineSchema({
  // CRM-owned: only the sync writes these rows.
  crm_accounts: defineTable(crm.accounts.schema)
    .index("by_external_id", ["external_id"])
    .source(crm.accounts),

  // App-owned: your mutations write these rows.
  customers: defineTable({ label: v.string(), note: v.string() }),

  // App-owned association between the two, with enforced cardinality.
  customer_accounts: defineTable({
    account_id: v.id("crm_accounts"),
    customer_id: v.id("customers"),
  })
    .index("by_account", ["account_id"])
    .link({ account_id: [0, 1], customer_id: [0, "many"] }),

  // Who counts as staff, for the rules in step 4.
  staff: defineTable({ member: v.string() }).index("by_member", ["member"]),
});
```

* `crm_accounts` is a [synced table](/integrations/synced-tables). Receiving
  the CRM's data does not make your app its owner: the generated types leave
  it out of `ctx.db`'s write methods, and a write that reaches the backend
  anyway fails with a `ReadOnlySource` error, `crm_accounts accepts changes
  only through its admitted source`. To change an account, you ask the CRM, as
  in [step 6](#6-change-it-through-an-operation).
* `customers` holds facts your app owns, such as an internal label and a note.
* `customer_accounts` is a [link type](/views/links). Each CRM account belongs
  to at most one customer (`[0, 1]`), and a customer can have any number of
  accounts. Bijection refuses any transaction whose final state breaks those
  bounds, whichever mutation makes it.

Relating accounts to customers is an ordinary mutation:

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

export const attach = mutation({
  args: { account_id: v.id("crm_accounts"), customer_id: v.id("customers") },
  handler: async (ctx, args) => {
    await ctx.db.insert("customer_accounts", args);
  },
});
```

Attaching an account that already belongs to another customer is refused
because of the link's bound. You don't write that check.

## 3. Define the business object

Your app wants to show a *customer*: its label, its note and how many CRM
accounts it has. Instead of assembling that in every query, declare it once as a
[view](/views/overview):

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineSchema, defineTable, defineView, q } from "bijection/server";

const accountCounts = q
  .table("customer_accounts")
  .groupBy(["customer_id"])
  .aggregate({ account_count: q.count() })
  .as("accounts");

export const Customer = defineView({
  key: { from: "customers", field: "_id" },
  expression: q
    .table("customers")
    .as("customer")
    .leftJoin(accountCounts, {
      left: "customer._id",
      right: "accounts.customer_id",
    })
    .select({
      label: q.field("customer.label"),
      note: q.field("customer.note"),
      account_count: q.field("accounts.account_count"),
    }),
});

export default defineSchema({
  // …the tables from step 2…
  Customer,
});
```

The view defines the `Customer` object type. Each row is keyed by the
customer's `_id`, and its type is inferred from the expression. Queries read it
with the same `ctx.db` calls as a table:

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

export const list = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("Customer").take(50);
  },
});
```

A client subscribed to `list` receives a new result when an account is
attached or a note changes, exactly as with any other
[reactive query](/realtime).

A customer with no accounts has no group to join, so its `account_count` is
absent rather than `0`.

You never write to a view. By default it is evaluated when read; adding
`.materialize()` stores its output and keeps it current in the same transaction
as every write to its inputs. Both forms return the same rows. See
[Materialized views](/views/materialized-views).

## 4. Protect it

In an ordinary backend, each function checks the caller before it reads.
Bijection lets you attach that decision to the data instead: an
[access rule](/access/overview) is a query you write once, and Bijection runs it
for every read of the tables it protects, from any function.

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

async function requireStaff(ctx: QueryCtx) {
  const identity = await ctx.auth.getUserIdentity();
  if (identity === null) throw new Error("Unauthenticated");
  const member = await ctx.db
    .query("staff")
    .withIndex("by_member", (q) => q.eq("member", identity.tokenIdentifier))
    .unique();
  if (member === null) throw new Error("Not staff");
}

// Staff may read customers, links and CRM accounts.
export const staffRead = internalQuery({
  args: readAccessArgs,
  returns: readAccessResults,
  handler: async (ctx, { requests }) => {
    await requireStaff(ctx);
    return requests.map(() => null);
  },
});

// What staff read may be copied only into these tables and into the CRM's
// rename command.
export const staffDisclose = internalQuery({
  args: disclosureArgs,
  returns: readAccessResults,
  handler: async (ctx, { requests, destination }) => {
    await requireStaff(ctx);
    const permitted =
      (destination.kind === "table" &&
        destination.component === null &&
        ["customers", "customer_accounts"].includes(destination.table)) ||
      (destination.kind === "command" &&
        destination.integration === "crm.js:crm" &&
        destination.command === "rename");
    if (!permitted) throw new Error("Customer data can't be released there");
    return requests.map(() => null);
  },
});

// Who may have the CRM rename an account: staff. Asked when a request is
// accepted and again before each send.
export const renameRule = internalQuery({
  handler: async (ctx, { requester }: { requester: string }) => {
    const member = await ctx.db
      .query("staff")
      .withIndex("by_member", (q) => q.eq("member", requester))
      .unique();
    if (member === null) throw new Error("Not staff");
    return [null] as [null];
  },
});
```

Attach the rules in the schema, to the tables and to the view:

```ts bijection/schema.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { makeFunctionReference, type ReadAccess } from "bijection/server";

const staffOnly: ReadAccess = {
  read: makeFunctionReference<"query">("access:staffRead"),
  disclose: makeFunctionReference<"query">("access:staffDisclose"),
  reads: ["staff"],
};

// crm_accounts: defineTable(…).index(…).source(crm.accounts).access(staffOnly),
// customers: defineTable(…).access(staffOnly),
// customer_accounts: defineTable(…).link(…).access(staffOnly),
// export const Customer = defineView({ … }).access(staffOnly);
```

Three separate decisions are at work:

* **Reading.** `staffRead` decides who may see the rows. A function called by
  anyone else fails with `Read access was refused`, whether it is a public
  query, an internal function or a new query someone adds next month.
* **Disclosing.** Once a function has read protected rows, anything it writes
  or sends may carry that data elsewhere. `staffDisclose` decides where it may
  go. Without a disclosure rule, a transaction that read these tables could not
  write at all, and the CRM command in step 6 would be refused.
* **Commanding.** `renameRule`, the command's `governingRule` from step 1,
  decides whose requests the CRM may receive. It gets the requester's token
  identifier, the target and the arguments, and refuses by throwing.

Rules are reactive: adding or removing a `staff` row reruns the subscriptions it
affects. Keep `staff` itself protected, with its own rules or behind internal
functions. [Read rules](/access/read-rules) and [Disclosure rules](/access/disclosure)
cover the full contract, and the [access model](/access/access-model) generates
rules like these from roles and grants.

## 5. Connect the account

Push the code (`bijection dev` does it for you). The synced table now exists,
empty. A deployment administrator connects it to a real CRM account:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection integration credential-create CRM_CREDENTIAL --from-file crm-credential.json
bijection integration configure crm --module crm.js --export crm \
  --base-url https://crm.example.com/api --credential-ref CRM_CREDENTIAL
bijection integration identify crm verify-1
bijection integration run verify-1
bijection integration install crm crm_accounts
```

The credential is read from a file, stored privately in the deployment and used
only by the host for the requests the definition declares. Your functions never
see it. `identify` records which CRM account the connection speaks for, and
every later sync is checked against that identity. After `install`, a sync of
the `accounts` collection is requested every five minutes and `crm_accounts`
fills in. The schedule requests work; it does not promise a completed sync every
five minutes while the CRM is unavailable.
See [Connecting and syncing](/integrations/connections).

A synced table reports what the source last proved, not the CRM's state at this
instant. [Coverage and freshness](/integrations/synced-tables#coverage-and-freshness)
shows how a query can tell how current it is.

## 6. Change it through an operation

Renaming an account is a business change that has to reach the CRM. It must not
be applied twice when a response is lost, a person may want to preview it
first, and only some users may make it. Declare it as an
[operation](/operations/overview):

```ts bijection/customers.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import { defineOperation } from "bijection/server";
import { v } from "bijection/values";
import { Customer } from "./schema";
import { crm } from "./crm";

export const renameAccount = defineOperation({
  on: Customer,
  args: {
    customer_id: v.id("customers"),
    account_id: v.id("crm_accounts"),
    name: v.string(),
  },
  returns: v.object({ call: v.string() }),
  prepare: async (ctx, args) => {
    const link = await ctx.db
      .query("customer_accounts")
      .withIndex("by_account", (q) => q.eq("account_id", args.account_id))
      .unique();
    if (link === null || link.customer_id !== args.customer_id) {
      throw new Error("This account does not belong to this customer");
    }
    await ctx.db.patch("customers", args.customer_id, {
      note: `Renamed account to ${args.name}`,
    });
    const call = await ctx.externalCalls.submit(crm.commands.rename, {
      target: args.account_id,
      args: { name: args.name },
    });
    return { call };
  },
});
```

`prepare` runs in one mutation transaction. The link check, the local note and
the intent to rename the account in the CRM **commit together, or not at all**.
Nothing is sent to the CRM during the transaction. The call is persisted first,
and the engine delivers it afterwards.

Every caller of an operation needs explicit grants: `invoke` to submit a
request, and `read` to read what it accepted, which includes the receipt `invoke`
returns and the `status` in step 7. A deployment administrator grants both to a
staff member, who also needs a `staff` row for the rules in step 4:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection permissions set 'https://auth.example.com|user_123' read true \
  --operation customers:renameAccount
bijection permissions set 'https://auth.example.com|user_123' invoke true \
  --operation customers:renameAccount
```

That user invokes the operation from your app through its Bijection `client`.
Each request carries a request key, chosen once when the user decides and kept
with the request (see
[Calling operations](/operations/calling-operations)):

```ts src/renameAccount.ts theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
import {
  operationFunctionReference,
  operationInvocationArgs,
} from "bijection/server";
import { api } from "../bijection/_generated/api";

const requestKey = crypto.randomUUID();
const receipt = await client.mutation(
  operationFunctionReference(api.customers.renameAccount, "invoke"),
  operationInvocationArgs(api.customers.renameAccount, requestKey, {
    customer_id: customerId,
    account_id: accountId,
    name: "Northwind Supplies Ltd",
  }),
);
```

The `preview` companion shows what the operation would change without
accepting, reserving or sending anything; it needs a separate `preview` grant.

<Note>
  The verified operator source build also supports the CLI path below. First
  configure [team or staff sign-in](/auth/team-members) and give your verified
  token identifier a `staff` row, as required by step 4. Administration alone
  gives no business authority. This path is not in the published 0.1.4 release.
  See [Granting yourself for `bijection run`](/access/permissions-cli#granting-yourself-for-bijection-run).
</Note>

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection permissions set --me read true --operation customers:renameAccount
bijection permissions set --me invoke true --operation customers:renameAccount
bijection run customers:renameAccount \
  '{"customer_id":"...","account_id":"...","name":"Northwind Supplies Ltd"}'
```

## 7. Follow the outcome

The receipt carries the request's invocation ID, and the operation's `status`
companion follows the request from there. Several distinct facts are
established, in order:

<Steps>
  <Step title="Accepted locally">
    The note and the external call committed. The receipt proves this, and
    nothing about the CRM.
  </Step>

  <Step title="Delivered">
    The engine sent `rename` to the CRM. Its answer is retained and read under
    the command's declared evidence, with an outcome such as `delivered`,
    `refused`, or `unknown` when the answer was lost.
  </Step>

  <Step title="Published">
    The confirmed account reaches `crm_accounts`, from the command's response
    when it proves the new record, or from the next sync. Only then do queries
    over `crm_accounts` show the new name.
  </Step>
</Steps>

Two answers can be lost, and neither means the rename failed:

* **Your app's.** If the response to `invoke` is lost, you don't know whether
  the request was accepted. Submit it again with the same request key and
  arguments: an accepted key returns its original receipt instead of preparing
  again. Never retry under a new key, because a new request key is a new
  business change and could rename twice. The `recover` companion looks up what
  a key accepted without submitting anything.
* **The CRM's.** If the CRM's answer is lost, the call's outcome is
  **unknown**. The engine reconciles it under the command's own contract:
  `reconcile` asks the CRM what became of that request identity, and because
  `rename` is idempotent, the engine may repeat the same request under the same
  identity. It never sends the rename under a new identity to find out.

[External calls](/operations/external-calls#unknown-outcomes-and-recovery)
lists every outcome.

## Try it on a branch first

A [branch](/branches/overview) forks the deployment's live data at one exact
moment. Operations you run on it change only the branch, and their external
calls are recorded and held, never delivered:

```sh theme={"theme":{"light":"github-light-default","dark":"github-dark-default"}}
bijection branch create rename-trial
```

When you [apply](/branches/apply-and-rebase#applying-to-live) the branch, the
operations you invoked on it run again against live's current data and your
current permissions. No rows are copied.

## What you built

* The CRM remains the owner of its accounts. Your app stores only its own facts
  and the relationships it has accepted.
* `Customer` is declared once and read everywhere with ordinary queries.
* Access is attached to the data, and reading is distinct from disclosing.
* A rename is one named business change with a stable identity. Its local effect
  and its external intent commit together, and its external outcome is followed
  until the CRM's evidence settles it.

Everything else in Bijection's function model still applies. Keep using
[queries](/functions/query-functions), [mutations](/functions/mutation-functions)
and [actions](/functions/actions) for work that owns no external business
change. [Understanding Bijection](/understanding/overview) explains how the
pieces fit together.
