Before 0.6.0 this connection was the relay module, with
createRelayHandler and a single RPC endpoint. The seam is the same; what changed is that its wire form is now a normative, versioned API. If you are upgrading, see the 0.6.0 entry in the changelog. The rename is mechanical, but both ends of a link must upgrade together.One contract, two implementations
The intuition behind the whole design is that Alfiz has exactly one load-bearing interface: the provider contract. Youralfiz client attaches to a provider and asks it for closure data, rows, requests, and org data; it neither knows nor cares what stands behind the interface. Everything Alfiz Cloud does rides on that indifference.
The contract exists as three artifacts, held in exact correspondence so they cannot drift:
- The interface.
AlfizProviderin@alfiz/core: the contract as a TypeScript type, what a Client attaches to. - The abstract class.
AlfizProviderBasein@alfiz/core: the contract as an implementation root. Exactly two kinds of provider extend it, by design:- the local provider (
AlfizApplication), the contract implemented against your database. Standalone, it is the org root: the complete system with no external dependency. - the hosted provider (
HostedProvider), the same contract with its far side reached over the Provider API: an API connection wrapped in the abstract class. It stores and decides nothing itself; every operation is forwarded to the serving side, which enforces its own integrity rules on remote operations exactly as on local ones. Delegation, never a second writer.
- the local provider (
- The OpenAPI document.
openapi/alfiz-provider.v1.yaml, shipped in the@alfiz/corepackage: the contract’s wire form, fixed as a language-agnostic document rather than a TypeScript export.
The wire, in five conventions
The whole API follows from the contract being method-shaped, so the API is too, with one path per operation mirroring the contract one-to-one, rather than a resource grammar bolted over an RPC contract:- Every operation is
POST {base}/v1/{op}with a JSON object body of the operation’s named parameters ({}when there are none). POST-only keeps the transport uniform and keeps authorization data out of URLs and shared caches. - Every success is a
200with a JSON object, never a bare array or primitive ornull, so any response can grow a field without a wire break. Void operations return{}. - Every failure carries a typed-error envelope under a non-2xx status. The status is a transport hint, and a correct one, so retry policies and dashboards read the API sensibly (
403not org root,409conflict or graph cycle,422validation,404not found,501unsupported), but the envelope is normative.ProviderWriteRejectedErrorcodes andGraphCycleErrorpaths survive the wire and re-throw intact on the calling side, so a dashboard renders “cycle: a → b → a” identically for local and remote writes. - Authentication is
Authorization: Bearer <token>, the secret minted at link time, compared in constant time. - The live
onInvalidatestream never crosses. The epoch operations (epoch.head,epoch.since) are the cross-process invalidation transport, exactly the mechanism the library already uses between processes sharing a database, carried over HTTP instead.
/v1/) increments only on a wire break; additive changes ride on the object-body convention instead.
The non-JS check path: POST /v1/check
The one deliberate exception to “the API is administrative traffic”: a service that cannot run the TypeScript client, in Go or Python or Java or anything else, may POST a check and have the serving Application evaluate it in-process, against the same catalog, rows, resolver, and closure caches every local check uses.
can: an array key is any-of, no scope means the global scope, fresh: true bypasses the serving side’s caches. Unknown keys answer with the typed-error envelope (UnknownPermissionError, a programming error to map to 500), and keys declared requiresCondition: true answer with MissingConditionError rather than a half-checked yes, because conditions are in-process predicates over resource state and cannot cross a wire.
What this does not change: “runtime checks never leave the application” survives intact, because the serving side is the Application, in your infrastructure, so nothing Alfiz operates is on the path, and nothing here is metered. The caller pays a network hop instead of an in-process call, which is the honest cost of not being in the process. Keep latency-sensitive polyglot callers close to the Application, and let TypeScript services keep using the in-process client.
Mount the handler
The handler ships in@alfiz/application. Operations arrive at per-operation paths, so mount it as a catch-all POST route wherever your Application runs:
src/app/api/alfiz/[...op]/route.ts
Event persistence is on by default since 0.7.0 whenever the storage driver supports it, so the health probe reports
hasEpoch: true on an ordinary deployment. Without it (events: { persist: false }, or a driver with no event methods) the connection still works. The probe reports hasEpoch: false and Alfiz Cloud falls back to the TTL bounds for its cached org reads instead of revalidating against your log. See caching and staleness.Set up the connection
1
Register the application
From the application page in the Dashboard, create the application entry, or call
POST /api/v1/orgs/{org}/applications with an admin key. Either path runs the same server function; the API is not a second implementation.2
Configure the connection URL and copy the secret
Point the entry at the base URL your handler is mounted below, since operations POST to
{base}/v1/{op}, and link it. The provider secret is shown once, at link time, so copy it into your deployment’s environment as ALFIZ_PROVIDER_SECRET. Losing it means re-linking, not recovering.3
Verify with the health probe
Run the connection health probe from the application page, or read
GET /api/v1/orgs/{org}/applications/{app}, which includes connection health and registry status. The probe is the ping operation, and returns { api, application, orgRoot, hasEpoch, auditOptIn }: it confirms the route is reachable and the secret matches, reports the Provider API version the serving side speaks (api: 1), and reports whether an event log was configured. hasEpoch is a static fact about how the Application was constructed, not a liveness signal.Handling authority transfer
orgRoot is a constructor commitment. createApplication records it once and the library never flips it at runtime. When you later promote to federation (or demote back), Alfiz Cloud pushes the new dataset over the Provider API and calls onAuthorityChanged(orgRoot) so your host process can rebuild its Application with the new flag:
src/app/api/alfiz/[...op]/route.ts
Consuming the API yourself
Both halves of the wire ship in@alfiz/application: the handler that serves it, and the hosted provider that consumes it. createHostedProvider gives you an AlfizProvider over fetch, which helps when an internal admin tool or a back-office service needs to administer an Application it has no database connection to:
ProviderTransportError, distinct from errors the provider itself threw.
Next steps
What linking means
What changes when you link, and the longer list of what does not. Authority stays with your Application; the Provider API only carries administrative traffic to it.
API endpoints
The full REST surface behind the Dashboard, including application registration, linking, and the health probe.