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.
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 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.
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
bijection/schema.ts
- Reading.
staffReaddecides 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. - Disclosing. Once a function has read protected rows, anything it writes
or sends may carry that data elsewhere.
staffDisclosedecides 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’sgoverningRulefrom 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.
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:
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:
client.
Each request carries a request key, chosen once when the user decides and kept
with the request (see
Calling operations):
src/renameAccount.ts
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’sstatus
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.- Your app’s. If the response to
invokeis 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. Therecovercompanion 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:
reconcileasks the CRM what became of that request identity, and becauserenameis idempotent, the engine may repeat the same request under the same identity. It never sends the rename under a new identity to find out.
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: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.