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

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 is a query you write once, and Bijection runs it for every read of the tables it protects, from any function.
bijection/access.ts
Attach the rules in the schema, to the tables and to the view:
bijection/schema.ts
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 and Disclosure rules cover the full contract, and the 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:
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. A synced table reports what the source last proved, not the CRM’s state at this instant. 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.ts
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:
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):
src/renameAccount.ts
The preview companion shows what the operation would change without accepting, reserving or sending anything; it needs a separate preview grant. To try the operation from the command line, call it as yourself. bijection run uses your own verified identity, the member signed in with bijection login, so grant it with --me. The rules in step 4 then need your staff row, keyed by that identity:
The deployment must trust your identity: add operatorIdentity() from bijection/server to the providers in bijection/auth.config.ts. Administrator credentials hold no operation permission, so without the grant bijection run is refused with OperationAccess. See Granting yourself for bijection run.

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:
1

Accepted locally

The note and the external call committed. The receipt proves this, and nothing about the CRM.
2

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

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.
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 lists every outcome.

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 the branch, and their external calls are recorded and held, never delivered:
When you apply 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, mutations and actions for work that owns no external business change. Understanding Bijection explains how the pieces fit together.