Skip to main content
An integration is a TypeScript declaration of how Bijection talks to one external system: which HTTP requests it may make, what each response proves, which collections it reads and how often. You write it with defineIntegration in a file inside your bijection/ directory. This integration reads customers from a provider that exposes an ordered change feed at GET changes?after=<checkpoint>:
bijection/crm.ts
It has four parts:
  • http declares every request the integration may make.
  • identify reads which provider account a connection speaks for.
  • collections declares each kind of record, its schema, and its sync.
  • commands declares the changes the integration can send back to the provider. This one declares none.
Nothing in the declaration names a host or a credential. The provider’s base URL and credential belong to the connection, so the same definition works against a test account and a production account.

Declaring an integration

defineIntegration must be imported by name from bijection/server, called directly, and assigned to an exported const:
bijection/crm.ts
The file’s path and the export name form the integration’s address, crm.js:crm here. Connections and tables refer to the integration by this address, so export each integration exactly once and keep it in the same module. Moving a definition to another module requires its installations to be recreated. The collections become properties of the returned object. crm.customers is the handle a synced table binds to. A collection can’t be named after a built-in property of the integration, such as commands.

Definitions from another directory

A definition written outside your bijection/ directory, for example in a shared package, has no address in your deployment. Admit it from a module of your deployment with admitIntegration:
bijection/storefront.ts
The admitted definition takes the admitting module’s address, storefront.js:storefront, and .source(...) binds its collections exactly as it binds a local definition. admitIntegration accepts only the imported definition itself, and a definition can be admitted from one module only.

HTTP contracts

Each entry of http is a named contract for one request. Your handlers make a request by naming its contract:
The host builds the request from the contract, sends it to the connection’s base URL with the connection’s credential, and retains the response before your code sees it. Your code never chooses the host, the path outside the declared route, or the credential.

Handlers

handler.member names the handler that owns the contract: A handler can only make requests through its own contracts. A request through a contract owned by another handler is refused.

Routes, queries and bodies

Route segments, query values, header values and body fields are templates. Use { kind: "literal", value } for a fixed value and { kind: "input", input } for a value the host supplies:
  • checkpoint: the change feed position the sync resumes from.
  • resume: the provider cursor a listing resumes from.
  • parameter: a value your code passes, checked against parameters.
  • operation_id, target_key, target_version, argument: values a command sends.
To pass values from your code, declare parameters and address them by path:
bijection/crm.ts

Responses

response.statuses lists each status the provider may answer and the body it carries:
  • { kind: "json", validator }: the body must parse and match the validator.
  • { kind: "none" }: no body.
  • { kind: "opaque" }: bytes that are retained but not interpreted, such as an HTML error page.
xml, jsonl and a few provider-specific shapes are also available. A status that is not listed is still retained, but proves only that the provider answered. ctx.http.request resolves to the status, the admitted headers, the typed body and an evidence reference to the retained response. The body type follows the validators you declared. For syncs, response.failures classifies error statuses so a failed read is retried or reported correctly:
The kinds are transient, unavailable, insufficient_scope, authentication, signature and throttled.

Evidence

evidence says what an admitted response proves, in terms of paths into its body. Bijection checks your handler’s result against it: a record your code returns must cite the response it came from and its position in that response, and its key must match the key the evidence reads. A handler can’t publish a record a response never contained. Other kinds, such as incremental, traversal, projections and query_result, describe more specialized provider APIs. The HttpEvidence type in bijection/server lists them all.

Verifying the account

identify reads which provider account a connection speaks for. It runs when you verify a connection, and the result is the identity every later sync is checked against.
bijection/crm.ts
It returns:
  • account_id: the provider’s identifier for the account, read at the path the identity evidence declares in account.
  • incarnation: optional. The provider’s own reset epoch, where it has one.
  • evidence: the response that proved it.
If the provider names no account anywhere, declare identity evidence without account and return only { evidence }. Returning an account_id the evidence doesn’t declare is refused.

Collections

Each collection declares a schema, a protocol and a sync.

Schema

schema is an object of validators, the same ones you use in defineTable. It is the exact shape of the table that binds to the collection. The field name source_id is reserved. Keep exact provider values exact. Store large identifiers, decimal amounts and positions as strings or v.int64(), and don’t convert them through JavaScript numbers on the way in.

Protocols

protocol states what the provider’s API actually guarantees. Bijection relies on it to decide what a sync may publish, so declare what the provider documents, not what you hope for.
change_feed is an ordered history of changes. Each change is a full replacement or a deletion of one record, at a position the provider states.
  • position.encoding is uint64_decimal or int64_decimal. Positions travel as decimal strings.
  • checkpoint.initial is where the first sync starts.
  • checkpoint.retention is how long the provider keeps a position resumable: { kind: "stated", unit, value } with a unit of minutes, hours or days, or { kind: "unknown" }.

The sync

sync declares how often the collection is read and how a response becomes records:
  • every is the interval: { seconds }, { minutes } or { hours }, at most 366 days. Bijection’s scheduler runs it; you don’t need a cron job. A schedule requests work: it doesn’t guarantee a completed sync every interval while the provider is down.
  • read(ctx, input) makes requests through its own contracts and returns what they observed. A single read may make at most 16 requests.
For a change feed, read receives { checkpoint } and returns the changes, each citing its response and its index within it, plus the provider’s progress:
For a paged listing, read receives the provider cursor as resume and returns the records it observed. This integration lists invoices a page at a time. The contract, under http:
bijection/billing.ts
And the collection, under collections:
bijection/billing.ts
Bijection keeps the returned next_resume, commits each page with the records it admitted, and fills the resume input of the next request with it until the listing is complete.
A read returns candidate records. It never writes to a table itself. Bijection checks the result against the retained responses and publishes it into the synced table in one transaction per page. A failed page publishes nothing: it is retried later, or marked blocked until you resume it (see Connecting and syncing).

Deletions

A record is removed from the synced table only when the provider says it is gone: a delete change in a feed, or a record your read returns with kind: "deleted". A record that is simply missing from a listing is kept.

Commands

commands declares the changes the integration can send back to the provider. Each command names the collection it targets, its typed args, the provider’s delivery and conditional-write contract, and send and reconcile handlers with their own HTTP contracts. Your application never calls a command directly. It requests one as a durable external call from a mutation, and Bijection sends it, reconciles its outcome and records it. See Operations.

Setup fields

Setup fields are in beta.
Some providers need more than a base URL and a credential, such as an account subdomain. Declare these values once with setup:
bijection/helpdesk.ts
The console’s connection form and bijection integration configure --show-setup are derived from this declaration, and the backend refuses values that don’t match it. A field with fills: "base_url" renders the connection’s base URL, so you don’t pass --base-url. Setup values are never secrets: a field marked secret: true only documents what the provider needs, and the value itself goes into a credential.

Provider rate limits

Declare the provider’s request allowance with budget. It belongs to the connection, so every collection and command of that connection spends from one bucket, and a request the bucket can’t hold waits until it can:
bijection/crm.ts
For a provider that states its allowance as a cost bucket, declare capacity with restorePerSecond or restorePerMinute instead, and optionally requestCost and the response fields under stated where the provider reports its own figures.
backoff is in beta.
backoff declares where the provider states when it will accept another request, such as a Retry-After header on a 429:
bijection/crm.ts
A wait longer than maxWaitSeconds holds the connection until an operator resumes it. A stated wait never makes Bijection resend a command.