Defining a contract
Every contract is built with defineContract(id, shape) from the pure
@axion/bridgekit/contract entrypoint. The id is validated against
/^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/ — lowercase, dot-separated, <owner>.<surface>.
import { defineContract, t } from '@axion/bridgekit/contract';
export const ConnectHost = defineContract('connect.host', { methods: { /* ... */ }, streams: { /* ... */ }, state: { /* ... */ },});The returned value is a frozen BridgeContract token. It carries a normalized
descriptor, a hash (FNV-1a 32-bit over the normalized descriptor), and a phantom
_shape type used only at compile time.
Two authoring styles
Section titled “Two authoring styles”BridgeKit accepts two ways to write a contract. They compile to identical runtime descriptors — pick by taste and by what your codegen path supports.
Runtime, schema-driven. The schema also drives boundary encoding, so the wire format is derived from the same definition. Great when you want named, reusable object schemas.
import { defineContract, t } from '@axion/bridgekit/contract';
export const ConnectHost = defineContract('connect.host', { methods: { showLogin: t.fire(), isLoggedIn: t.query(t.boolean()), installEsim: t.query( t.object({ url: t.string(), iccId: t.string() }), t.literals('success', 'already-installed', 'cancelled', 'error'), { timeoutMs: null }, ), getDeviceInfo: t.querySync(t.object({ model: t.string(), osVersion: t.string() })), trackEvent: t.fire(t.object({ name: t.string(), params: t.json() })), }, streams: { otpCodes: t.stream(t.string()) }, state: { connectivity: t.state(t.object({ online: t.boolean() }), { online: false }) },});Type-first. Each member is a generic marker whose type parameters carry the params and
result. Markers are phantom — the only runtime payload is { kind } (plus initial for
State). This is the style the live demo uses.
import { Async, defineContract, State, Stream, Void } from '@axion/bridgekit/contract';
export const useDemoHost = defineContract('bridgekit.demo-host', { methods: { ping: Async<{ message: string }, { reply: string; epoch: number }>(), increment: Async<number>(), say: Void<{ text: string }>(), }, streams: { ticker: Stream<number>(), echoes: Stream<string>(), }, state: { counter: State<number>(0), },});The marker ⇄ DSL mapping is one-to-one:
| Marker | Schema DSL | Kind |
|---|---|---|
Sync<P, R>() | t.querySync(params, result) | querySync |
Async<P, R>() | t.query(params, result) | query |
Void<P>() | t.fire(params) | fire |
Stream<V, P>() | t.stream(value, opts) | stream |
State<V>(initial) | t.state(value, initial) | state |
Direction is a matter of who implements
Section titled “Direction is a matter of who implements”A contract does not encode direction in its definition — direction is decided by which side provides. The same shape works both ways:
// provided by NATIVE, consumed by RNexport const ConnectHost = defineContract('connect.host', { /* ... */ });
// provided by RN, consumed by NATIVE — same shape, reverse directionexport const LiaFeature = defineContract('lia.feature', { methods: { getUnreadCount: t.query(t.number()) }, state: { sessionStatus: t.state(t.literals('idle', 'active', 'error'), 'idle') },});The purity rule (enforced)
Section titled “The purity rule (enforced)”@axion/bridgekit/contract is a pure, side-effect-free entrypoint: defineContract,
the t namespace, the markers, and type utilities — nothing else. A contract file may
import only this subpath and other contract files.
// ✅ allowedimport { defineContract, t } from '@axion/bridgekit/contract';import { PickedFile } from './shared-schemas.contract';
// ❌ rejected by the CLI with file:lineimport { analytics } from '../analytics';The CLI walks the import graph before evaluating and fails with file:line on any
violation. Why it matters: it lets the generator, Jest, and web builds import contracts
safely — the transport-connecting side effect lives in the native runtime entry
(index.native.ts), not in the contract module.
Type utilities
Section titled “Type utilities”Object schemas can be hoisted, named, and reused across contracts. Inference helpers expose the derived TypeScript types:
const PickedFile = t.object({ uri: t.string(), mime: t.string() });
type Picked = t.Infer<typeof PickedFile>;type Params = MethodParams<typeof ConnectHost, 'installEsim'>;type Result = MethodResult<typeof ConnectHost, 'installEsim'>;type Net = StateValue<typeof ConnectHost, 'connectivity'>;Next: the full schema DSL, then methods, streams & state in depth.