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
- 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.
idempotentsays the CRM deduplicates requests by their identity for 90 days, so a lost response can be retried under the same identity.conditionsays the CRM checks the record version the request was based on.reconcileis how Bijection later asks the CRM what became of a request.governingRulenames 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_accountsis a synced table. Receiving the CRM’s data does not make your app its owner: the generated types leave it out ofctx.db’s write methods, and a write that reaches the backend anyway fails with aReadOnlySourceerror,crm_accounts accepts changes only through its admitted source. To change an account, you ask the CRM, as in step 6.customersholds facts your app owns, such as an internal label and a note.customer_accountsis 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.
bijection/customers.ts
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
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
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:assignments table in your schema. Declare your
resources and roles in access.ts:
bijection/access.ts
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
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
readpermissions decide who may see the rows. A function called by anyone else fails withRead 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.
linklets an account’s ID into a link,notelets a rename record a note on its customer, andrenamelets 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’sgoverningRulefrom step 1, decides whose requests the CRM may receive: whoever holdsrenameon the account. It reads the grants as a private input, and refuses by throwing.
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:
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:
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
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_idis 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.refuserecords 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.
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
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:
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. Itsresult.delivery is the original Connections reference.
Subscribe to an ordinary query to follow that delivery:
bijection/deliveryStatus.ts
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.
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 inheld mode, so no provider request is sent.
is_review_required: true, operation run
returns a pending request. Prepare and inspect that original request before
selecting its exact candidate:
What you built
- The CRM remains the owner of its accounts. Your app stores only its own facts and the relationships it has accepted.
Customeris 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.