SDK & API

Routes, data and sync API

Source-derived routes, records and sync APIs with checked local writes and complete acknowledgement/change-page examples.

Alpha documentation0.6.0-alpha.1

Contract and use

The route client returns HTTP Response objects. Data, sync and security wrappers return CappResult envelopes. Treat their types as transport contracts, not proof of persistence or remote convergence. Use the notes walkthrough to supply matching application, route and collection declarations.

Request and result semantics

OperationInspect before using it
routes.request(path, init)response.ok, status, validated body and intended transport. A resolved 503 is still failure.
data.collection(name)A handle only; it neither creates the collection nor grants access.
list/get/put/append/deleteresult.ok, the actual supported bridge method and expected payload/record evidence.
metadata(key)A capp.data-record.v1 record with key, revisions and sync state; includes tombstone inspection.
sync.status/conflictsCurrent queue/conflict state; these are not requests to resend.
sync.flush/retry/pull/resolveExplicit operations that may contact the backend or change local data.

The SDK's put() can echo the submitted value when the bridge supplies no normalized value. That echo is not a read-back. Some receipt envelopes require inspecting raw or using metadata() rather than assuming value is already your domain object.

Save and observe a local record

This helper is for the notes collection defined in the walkthrough. It takes the known server base revision, validates the note through the configured schema, checks the writer, and requires a real metadata record after the write. Invoke it once for an explicit save; handle its rejected promise in your UI.

ts
import type { CappClient, CappDataRecord } from '@capp/app-sdk';

type DraftNote = { id: string; title: string };
export async function saveDraftNote(
  client: CappClient, note: DraftNote, baseRevision: number,
): Promise<CappDataRecord<DraftNote>> {
  if (!Number.isSafeInteger(baseRevision) || baseRevision < 0) {
    throw new Error('A nonnegative server base revision is required');
  }
  const bridge = client.currentRuntime()?.data;
  if (typeof bridge?.write !== 'function' ||
      (typeof bridge.metadata !== 'function' && typeof bridge.read !== 'function')) {
    throw new Error('Local record writing and metadata are unavailable');
  }
  const notes = client.data.collection<DraftNote>('notes');
  if (!notes.contract()) throw new Error('The notes collection is not declared');
  const saved = await notes.put(note.id, note, { baseRevision });
  if (!saved.ok || saved.raw === undefined) throw new Error('Local save was not confirmed');
  const observed = await notes.metadata(note.id);
  const record = observed.value;
  if (!observed.ok || record?.protocol !== 'capp.data-record.v1' ||
      record.collection !== 'notes' || record.key !== note.id ||
      !Number.isSafeInteger(record.localRevision) || record.localRevision < 1 ||
      record.deletedAt || record.value?.id !== note.id || record.value.title !== note.title) {
    throw new Error('Expected local record was not observed; inspect before retrying');
  }
  return record;
}

A new record normally starts from server revision zero; an existing record must use its observed server revision rather than a guessed constant. The returned pending state means saved on this device, not acknowledged by the server. A rejected observation after a write is an uncertain result: inspect the stored record before retrying. This helper does not claim an atomic compare-and-swap or protect against another writer changing the record immediately afterwards.

Acknowledgements and pull pages

For collection replay, the runtime sends the value as the mutation body and identity/revision information in headers. The backend should validate and correlate x-capp-sync-op-id, x-capp-idempotency-key, collection/key, base/local revision and content hash where supplied. Those headers are input, not proof of user authorization.

After the authenticated mutation has been durably applied, a minimal correlated acknowledgement can be:

json
{
  "protocol": "capp.sync-ack.v1",
  "operationId": "example-operation-1",
  "status": "applied",
  "collection": "notes",
  "key": "note-1",
  "revision": 1,
  "value": { "id": "note-1", "title": "Inspect the garden" }
}

Replace the example identity with the incoming operation's identity and return the authoritative revision. duplicate refers to the same saved operation, deleted acknowledges its tombstone, and conflict must use HTTP 409 in the current transport. A required acknowledgement can be the body itself or its ack/result field. HTTP 200 without it is not an acknowledgement when requiresAck is set.

A pull endpoint returns a bounded page of changes:

json
{
  "protocol": "capp.sync-page.v1",
  "collection": "notes",
  "changes": [{
    "key": "note-1", "revision": 1,
    "value": { "id": "note-1", "title": "Inspect the garden" }
  }],
  "cursor": "example-cursor-1",
  "hasMore": false
}

Cursors are backend-owned positions, not timestamps guessed by the client. The runtime bounds pages per pull (default 20, clamped to 1–100 with maxPages). A successful bounded pass can still leave later pages: it is not proof that every remote change has been retrieved. Retain tombstones and authoritative revisions in change pages.

Conflict decisions and alpha limits

collection.pending() filters the current queue, but its name does not guarantee every returned entry literally has status pending. Inspect status and last error. collection.conflicts() selects that collection's conflict records.

Use sync.resolve(id, {strategy: 'remote'}) only after choosing the server version; local retains local intent and can require another push; custom requires validated merged content. Do not automatically call retry() from a polling status component. A valid JSON shape does not prove a permitted state transition.

The current SDK/native combination does not provide a full acknowledgement-correlation validator, a general migration engine or exactly-once business effects. Plain ok flags and queue counts must not authorize settlement. The public schemas cover structure; use authoritative server storage and source-tested adapters for stronger requirements.

CappRouteRequestInit

ts
export type CappRouteRequestInit = RequestInit & {
    capability?: string;
    execution?: CappExecutionMode | string;
    preferLocal?: boolean;
    localOnly?: boolean;
    preferOffline?: boolean;
    offline?: boolean;
    preferRemote?: boolean;
    remoteOnly?: boolean;
};

CappRoutesClient

ts
export declare class CappRoutesClient {
    constructor(client: CappClient);
    policyFor(path: string, method?: string): CappRoutePolicyEntry | undefined;
    request(path: string, init?: CappRouteRequestInit): Promise<Response>;
    assertion(path: string, method: string, capability?: string, bodySha256?: string): Promise<string | undefined>;
}

CappDataOperationOptions

ts
export type CappDataOperationOptions = {
    includeDeleted?: boolean;
    baseRevision?: number;
    sync?: boolean;
    metadata?: Record<string, unknown>;
};

CappDataCollection

ts
export declare class CappDataCollection<T = unknown> {
    readonly name: string;
    constructor(dataClient: CappDataClient, name: string);
    contract(): CappCollectionContract | undefined;
    list(options?: CappDataOperationOptions): Promise<CappResult<T[]>>;
    get(key: string, options?: CappDataOperationOptions): Promise<CappResult<T>>;
    put(key: string, value: T, options?: CappDataOperationOptions): Promise<CappResult<T>>;
    append(value: T, options?: CappDataOperationOptions): Promise<CappResult<T>>;
    delete(key: string, options?: CappDataOperationOptions): Promise<CappResult<unknown>>;
    metadata(key: string): Promise<CappResult<CappDataRecord<T>>>;
    pending(): Promise<CappResult<Record<string, unknown>[]>>;
    conflicts(): Promise<CappResult<Record<string, unknown>[]>>;
}

CappDataClient

ts
export declare class CappDataClient {
    readonly client: CappClient;
    constructor(client: CappClient);
    collection<T = unknown>(name: string): CappDataCollection<T>;
    contract(name: string): CappCollectionContract | undefined;
    list<T = unknown>(collection: string, options?: CappDataOperationOptions): Promise<CappResult<T[]>>;
    get<T = unknown>(collection: string, key: string, options?: CappDataOperationOptions): Promise<CappResult<T>>;
    put<T = unknown>(collection: string, key: string, value: T, options?: CappDataOperationOptions): Promise<CappResult<T>>;
    append<T = unknown>(collection: string, value: T, options?: CappDataOperationOptions): Promise<CappResult<T>>;
    delete(collection: string, key: string, options?: CappDataOperationOptions): Promise<CappResult<unknown>>;
    metadata<T = unknown>(collection: string, key: string): Promise<CappResult<CappDataRecord<T>>>;
}

CappSyncClient

ts
export declare class CappSyncClient {
    constructor(client: CappClient);
    enqueue(operation: Record<string, unknown>): Promise<CappResult>;
    status(): Promise<CappResult>;
    flush(options?: Record<string, unknown>): Promise<CappResult>;
    conflicts(): Promise<CappResult>;
    retry(id: string): Promise<CappResult>;
    pull(collection?: string, options?: Record<string, unknown>): Promise<CappResult>;
    resolve(id: string, resolution: {
        strategy: 'local' | 'remote' | 'custom';
        value?: unknown;
    }): Promise<CappResult>;
}

CappSecurityClient

ts
export declare class CappSecurityClient {
    constructor(client: CappClient);
    installIdentity(): Promise<CappResult<CappInstallIdentity>>;
}

CappRoutePolicyEntry

ts
export type CappRoutePolicyEntry = {
    path: string;
    methods?: readonly string[];
    capability?: string;
    risk?: CappRisk;
    group?: string;
    online?: boolean;
    offline?: boolean;
    offlineCollection?: string | null;
    localHandler?: string | null;
    sync?: string;
    sdk?: string | null;
    enabled?: boolean;
    excluded?: boolean;
    reason?: string | null;
    execution?: CappExecutionMode | string;
    metadata?: Record<string, unknown>;
};

CappRoutePolicyDocument

ts
export type CappRoutePolicyDocument = {
    protocol?: string;
    siteId?: string | null;
    appId?: string | null;
    namespace?: string;
    mode?: 'online-only' | 'hybrid' | 'offline-first' | string;
    routes?: readonly CappRoutePolicyEntry[];
};

CappCollectionContract

ts
export type CappCollectionContract = {
    id: string;
    description?: string;
    keyField?: string;
    schema?: CappJsonSchema;
    capabilities?: readonly string[];
    sync?: CappCollectionSyncContract;
    revisions?: {
        enabled?: boolean;
        authority?: 'server' | 'local' | 'hybrid';
        monotonic?: boolean;
    };
    tombstones?: boolean | {
        enabled?: boolean;
        retainUntilAcknowledged?: boolean;
    };
    metadata?: Record<string, unknown>;
};

CappDataContractDefinition

ts
export type CappDataContractDefinition = {
    protocol?: typeof CAPP_DATA_CONTRACT_PROTOCOL | 'capp.data-contract.v1' | string;
    namespace?: string;
    collections: Record<string, Omit<CappCollectionContract, 'id'> & {
        id?: string;
    }> | readonly CappCollectionContract[];
    metadata?: Record<string, unknown>;
};

CappDataRecord

ts
export type CappDataRecord<T = unknown> = {
    protocol: typeof CAPP_DATA_RECORD_PROTOCOL;
    collection: string;
    key: string;
    value: T | null;
    revision: number;
    baseRevision: number;
    localRevision: number;
    contentHash?: string | null;
    deletedAt?: string | null;
    createdAt?: string;
    updatedAt: string;
    syncState: CappDataSyncState;
    lastOperationId?: string | null;
    conflict?: unknown;
    metadata?: Record<string, unknown>;
};

CappSyncOperation

ts
export type CappSyncOperation = {
    protocol: typeof CAPP_SYNC_OPERATION_PROTOCOL;
    id: string;
    createdAt: string;
    updatedAt: string;
    status: 'pending' | 'retry' | 'conflict' | string;
    attempts: number;
    idempotencyKey: string;
    path: string;
    method: string;
    capability?: string | null;
    collection?: string | null;
    key?: string | null;
    body?: unknown;
    baseRevision?: number;
    localRevision?: number;
    contentHash?: string | null;
    deletedAt?: string | null;
    sync?: string;
    lastStatus?: number;
    lastError?: string;
    lastResponse?: unknown;
};

CappSyncAck

ts
export type CappSyncAck<T = unknown> = {
    protocol: typeof CAPP_SYNC_ACK_PROTOCOL;
    operationId: string;
    status: 'applied' | 'duplicate' | 'deleted' | 'conflict';
    collection?: string;
    key?: string;
    revision?: number;
    contentHash?: string | null;
    deletedAt?: string | null;
    value?: T;
    cursor?: string | number | null;
    metadata?: Record<string, unknown>;
};

CappSyncPage

ts
export type CappSyncPage<T = unknown> = {
    protocol: typeof CAPP_SYNC_PAGE_PROTOCOL;
    collection?: string;
    changes: readonly CappSyncChange<T>[];
    cursor?: string | number | null;
    hasMore?: boolean;
    metadata?: Record<string, unknown>;
};

CappSyncConflict

ts
export type CappSyncConflict<T = unknown> = {
    protocol: typeof CAPP_SYNC_CONFLICT_PROTOCOL;
    id: string;
    collection: string;
    key: string;
    source: 'push' | 'pull' | string;
    createdAt: string;
    local?: CappDataRecord<T> | null;
    remote?: CappSyncChange<T> | null;
    operationId?: string | null;
    baseRevision?: number;
    remoteRevision?: number;
    status: 'conflict' | 'resolved';
    metadata?: Record<string, unknown>;
};

CappJsonSchema

ts
export type CappJsonSchema = {
    $schema?: string;
    $id?: string;
    title?: string;
    description?: string;
    type?: CappJsonPrimitiveType | readonly CappJsonPrimitiveType[];
    properties?: Record<string, CappJsonSchema>;
    /** Alpha compatibility with the pre-V248 data planner. Prefer properties. */
    fields?: Record<string, CappLegacyFieldType>;
    required?: readonly string[];
    additionalProperties?: boolean | CappJsonSchema;
    items?: CappJsonSchema;
    enum?: readonly unknown[];
    const?: unknown;
    minLength?: number;
    maxLength?: number;
    minimum?: number;
    maximum?: number;
    minItems?: number;
    maxItems?: number;
    pattern?: string;
    format?: 'date-time' | 'date' | 'uuid' | string;
    [key: string]: unknown;
};

validateCappValue

ts
export declare function validateCappValue(schema: CappJsonSchema | undefined | null, value: unknown, rootPath?: string): CappSchemaValidationResult;

normalizeCappSchema

ts
export declare function normalizeCappSchema(schema?: CappJsonSchema | null): CappJsonSchema;

SDK overview · Build with Sidecars · Security and authority

↑ ↓ to navigate · Enter to open · Esc to close. Search stays in your browser.