Skip to main content
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: 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.
The walkthrough assumes you have installed the CLI 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.

1. Describe the source

An integration 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.
bijection/crm.ts
Defining integrations shows the complete request contracts and handlers, including a command. 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.
  • No address or credential appears. They belong to the connection you create in step 5, 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:
bijection/schema.ts
  • crm_accounts is a synced table. 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.
  • customers holds facts your app owns, such as an internal label and a note.
  • customer_accounts is a link type. 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. opposite: "accounts" names the navigation from a customer to its linked accounts, used in step 6.
Relating accounts to customers is an ordinary mutation:
bijection/customers.ts
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:
bijection/schema.ts
The view defines the Customer object type. Each callback receives the typed fields of the table it reads, so a misspelled field fails to type-check. 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:
bijection/customers.ts
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. via follows the link’s accounts navigation, so you don’t name the join keys. It counts the CRM accounts the customer is linked to that the CRM still has; a customer with no accounts counts 0. You never write to a view. By default it is evaluated when read; declaring storage: { maintenance: "transactional" } 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.

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 model declares who may do what once, and Bijection enforces it on every read and write of the tables it protects, from any function. Before you connect the account in step 5, initialize access:
It signs you in to your app and makes you its first owner, which it can do only while the protected tables are empty. It installs the application access endpoints and the protected assignments table in your schema. Declare your resources and roles in access.ts:
bijection/access.ts
The delivery gateway’s requireRenameController checks the actual retained program and its original Operations admission or receipt. Its senderCalls declares those exact query dependencies. It returns the original requester’s identity with a finite deadline; it does not authenticate as that requester. The current account:rename check above still decides permission. Configure that gateway as in External calls. Bind each document and field to its resource permissions in the schema:
bijection/schema.ts
A link belongs to its customer (parent), so a role on a customer covers its links. The Customer view needs no binding: reading it reads its inputs, and their rules decide. Three separate decisions are at work:
  • Reading. A role’s read permissions decide 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.
  • Releasing. Once a function has read protected rows, anything it writes or sends may carry that data elsewhere. Rows may flow into their own table and their children’s; anywhere else needs a declared release. link lets an account’s ID into a link, note lets a rename record a note on its customer, and rename lets the rename in step 6 send what it read to the CRM. A release to a command is decided for the request’s requester when it is accepted and again before each send. Anything else is refused, and the function log names what was read and where it went.
  • Commanding. renameRule, the command’s governingRule from step 1, decides whose requests the CRM may receive: whoever holds rename on the account. It reads the grants as a private input, and refuses by throwing.
Decisions are reactive: a grant or a revocation reruns the subscriptions it affects. Access model covers roles, sharing and releases in full.

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:
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. source connect retains verification and installation work; closing the CLI leaves accepted setup running. The wait observes the first publication within its deadline. After connection, 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. When a private credential changes, repair the same connection:
The setup owner verifies the same CRM identity and recovers affected reads while keeping published facts. If the submission reply is lost, use bijection connection credential recover crm; it reads the original repair and never resends the secret. A synced table reports what the source last proved, not the CRM’s state at this instant. source status separates published coverage from current update progress. source refresh selects an exact refresh sequence. The native source acquisitions command exposes its finite observation window; the source-health component owns retained reports and stable history pagination. 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:
bijection/customers/renameAccount.ts
Configure operations as described in Defining operations, and install the deliveries gateway with its current access and resource policies as shown in External calls. Assessment runs the retained handler and discards its trial writes. At acceptance, the link check, local note, delivery request and Operations receipt commit together, or not at all. Nothing is sent to the CRM inside that transaction. Connections schedules the accepted delivery afterwards.
  • customer_id is an ordinary typed argument naming the customer being changed.
  • ctx.db.related(args.customer_id, "accounts").has(args.account_id) follows the link from step 2 and checks the selected account.
  • ctx.refuse records a business refusal for the original request. An unexpected error or authorization failure throws and cannot leave a partial acceptance.
  • The handler returns the full original delivery reference. Its routing and current authorization remain with Connections after deployment changes.
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. As the owner, give a staff member the staff role from step 4 and both permissions on this operation:
operation:viewer grants read alone. Revoke a grant with bijection access revoke <grant>; bijection access list shows its ID. That user invokes the operation from your app through its Bijection client. Each request carries a request ID, chosen once when the user decides and kept with the request, so a lost response is recovered rather than applied twice (see Calling operations):
src/renameAccount.ts
Keep expected_revision from the read that opened the edit. Review and retry keep that same value. Reading a newer revision and silently replacing it would accept a decision the user never made. Start app-owned customers at 0n; each accepted edit increments the revision in the same transaction. In React, the Operations component retains the draft and original request. A stable interactionKey restores the edit with its original revision:
After state.ready, bind the typed field’s getSnapshot() and edit() commands, and submit through form.handleSubmit. After a lost reply, use form.inspect() or explicitly form.resume() on that original request. prepare() follows immutable admission and its candidate requires explicit acknowledgement; the old editable native preview companion is retired. See Forms.
The current operator source supports the CLI path below. The owner holds every permission, so once team or staff sign-in is configured you can run the operation as yourself. Administration alone gives no business authority. Use a CLI built from the same source revision as these examples; public release availability may lag the source reference.

7. Follow the outcome

The Operations receipt proves that the local note and delivery request committed. Its result.delivery is the original Connections reference. Subscribe to an ordinary query to follow that delivery:
bijection/deliveryStatus.ts
The observations have separate owners: Operations acceptance stays accepted while delivery progresses. An uncertain provider outcome is not a failed local transaction. A cached success can prove the original rename while a newer independent CRM value stays current. Two responses can be lost:
  • Your app’s acceptance response. Recover through the same retained client request and original arguments. An accepted request returns its original receipt and full delivery reference. A new request is a new business change.
  • The CRM’s response. Keep the original delivery reference. Connections may reconcile through the captured command and, for an idempotent declaration, resend the original immutable key and payload within its remaining bounds. Cancellation or expiry cannot prove that a possible earlier write did not happen.
Observation is reactive and sends no provider request. Explicit cancellation, reconciliation and continuation revocation use ordinary application mutations under current policy and the inspected delivery revision. See External calls.

Try it on a branch first

A branch forks the deployment’s live data at one exact moment. Operations you run on it change only that branch. Connections records its external deliveries in held mode, so no provider request is sent.
The returned request reference retains the original branch generation, inputs and acceptance. Inspect it under your current permissions to recover the receipt and follow its held Connections delivery. For an operation defined with is_review_required: true, operation run returns a pending request. Prepare and inspect that original request before selecting its exact candidate:
Trial writes are discarded during preparation. Acceptance commits the local change and held delivery together. Retrying keeps the original request and candidate; editing the requested arguments requires an explicit new request. Branch program publication has its own review and explicit destination. It does not replay accepted business requests into live data.

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, mutations and actions for work that owns no external business change. Understanding Bijection explains how the pieces fit together.