Connect SDK

Contracts and adapters

Build domain behavior on declared contracts across different frontmatter fields and path conventions.

Every mdbase.contract has a stable ID, semantic version, exact digest, and JSON Schema. Its contract_type says what it describes: a portable record view, an emitted event, or an invocable action.

Only record contracts are implemented by type files. A type opts in through implements and maps contract field names to its own frontmatter fields. Contracts should usually be compact semantic interfaces, not copies of a storage or interchange format. This is intentionally many-to-many: one type can implement several contracts, reads union every approved implementing type, and several applications can consume the same portable record view.

Implement a contract in the type file

_types/project_item.md
kind: mdbase.type
name: project_item
version: 1
schema:
  dialect: json-schema-2020-12
  value:
    type: object
    required: [heading, stage]
    properties:
      heading: { type: string, minLength: 1 }
      stage: { type: string }
implements:
  - contract: example.work-item
    version: 1.0.0
    fields:
      title: heading
      status: stage
    binding:
      status:
        completed_values: [done, cancelled]

The type schema above governs stored frontmatter. The fields object is a direct mapping from contract fields to local type fields; it does not transform values. The optional binding object records behavior and vocabulary choices defined by the contract's binding_schema. For example, a task contract can let a collection declare several status values as complete without changing the stored status value.

General-purpose editors can safely expose both objects because their shapes come from the contract's JSON Schemas. Domain applications may offer more tailored controls and write the same implements entry. Unknown fields and extensions should be preserved.

Read the approved collection description

contract.ts
const description = await connection.describe();

const workItemContract = description.contracts.find(
  (contract) =>
    contract.id === "example.work-item" &&
    contract.version === "1.0.0"
);

if (!workItemContract) {
  throw new Error("The approved collection lacks example.work-item 1.0.0");
}

for (const implementation of workItemContract.implementations) {
  console.log(
    implementation.type_name,
    implementation.fields
  );
}

describe() returns each exact record-contract version, schema, digest, and complete implementations list. Each implementation includes its concrete type, field mapping, binding metadata, and digest. Use this data after authorization; do not look for semantic meaning in x-* extensions.

Keep your adapter on the transport-neutral client

worklog-collection.ts
import type { MdbaseCollectionClient } from "@mdbase/connect";

export class WorklogCollection {
  constructor(
    private readonly client: MdbaseCollectionClient<WorkItem>,
    private readonly contract: WorkItemContract
  ) {}

  list() {
    return this.client.queryAll({
      contract: {
        id: "example.work-item",
        version: "1.0.0"
      }
    });
  }
}

Domain adapters that use collection operations should accept MdbaseCollectionClient. This makes the adapter usable with Connect, createSandbox(), offline transports, and conforming providers.

A contract query returns normalized contract fields and unions every approved implementation. If a record exposes several approved views, pass the exact { id, version, type } selector. Creates also need an exact provider when more than one implementation is available.

Keep domain behavior with its application

Connect publishes application-neutral authorization, transport, schemas, and operation clients. A domain application owns its contract vocabulary, model, and adapter package. This keeps application conventions out of the Connect protocol and lets more than one app interpret the same generic collection independently.

Contract scope and full collection access

Contract-scoped grants expose only normalized mapped fields. Markdown bodies, unmapped frontmatter, unrelated types, and collection-wide features stay private. Filtering and sorting normalized fields happens in the application. Request full_collection only for genuine whole-record or collection-level behavior such as body-aware task tools, arbitrary saved views, schema editors, or general-purpose collection tools.

Event sources and action providers declare executable implementations through the interoperability bridge, not through a type file.