enbox docs
Packages

@enbox/api

The high-level TypeScript SDK for decentralised identity and data — typed protocols, records CRUD, real-time subscriptions, and anonymous queries.

@enbox/api is the main package developers use to interact with the Enbox network. It provides a type-safe, protocol-scoped API for creating, reading, updating, deleting, and subscribing to records stored in Decentralised Web Nodes (DWNs).

Installation

bun add @enbox/api

Core concepts

Enbox

The Enbox class is the entry point. You create an instance with Enbox.connect():

import { Enbox } from '@enbox/api';

const enbox = Enbox.connect({ agent, connectedDid: did });

The agent manages signing keys, DWN sync, and identity storage. The connectedDid is the user's DID. If you use @enbox/auth for authentication, you can pass the session directly:

const enbox = Enbox.connect({ session });

The Enbox instance exposes:

Property / MethodDescription
enbox.using(protocol)Returns a TypedEnbox scoped to a protocol
enbox.didDID API — create and resolve DIDs
enbox.vcVerifiable Credentials API
enbox.agentThe underlying EnboxAgent
enbox.disconnect()Stops sync and clears cached instances

There is no public dwn property on the Enbox instance. All DWN access goes through enbox.using(protocol).

defineProtocol

defineProtocol() creates a typed protocol definition that carries compile-time type metadata:

import { defineProtocol } from '@enbox/api';

interface TaskData {
  title: string;
  done: boolean;
}

const TaskProtocol = defineProtocol({
  protocol:  'https://example.com/tasks',
  published: true,
  types: {
    task: {
      schema:      'https://example.com/schemas/task',
      dataFormats: ['application/json'],
    },
  },
  structure: {
    task: {},
  },
}, {} as {
  task: TaskData;
});

The second argument is a phantom schema map — it exists only at compile time to map protocol type names to TypeScript interfaces. The runtime value ({}) is ignored.

TypedEnbox

TypedEnbox is the primary developer interface. You get one by calling enbox.using(protocol):

const tasks = enbox.using(TaskProtocol);

Instances are cached by protocol URI — calling using() multiple times with the same protocol returns the same instance.

Before performing record operations, call configure() to install the protocol on the local DWN:

const { status } = await tasks.configure();
// status.code: 200 (already installed) or 202 (newly installed)

If you skip configure(), TypedEnbox will auto-configure on the first record operation by querying for an existing installation and installing if needed.

TypedEnbox.records

All record methods auto-inject protocol, protocolPath, and schema — you only provide the path and your data:

create

const { status, record } = await tasks.records.create('task', {
  data: { title: 'Buy milk', done: false },
});
// record is TypedRecord<TaskData>

Create options:

FieldTypeDescription
dataT (type-checked)The record payload
parentContextIdstring?Link to a parent record's contextId
publishedboolean?Whether the record is publicly readable
recipientstring?DID of the intended recipient
protocolRolestring?Role for permission-scoped writes
tagsRecord<string, ...>?Indexed key-value metadata
storeboolean?Persist locally (default true)
encryptionboolean?Force or skip encryption
dataFormatstring?MIME type override

query

const { records, cursor } = await tasks.records.query('task');

for (const r of records) {
  const data = await r.data.json(); // TaskData
  console.log(data.title, data.done);
}

Query with filters, sorting, and pagination:

import { DateSort } from '@enbox/dwn-sdk-js';

const { records, cursor } = await tasks.records.query('task', {
  filter: { tags: { done: false } },
  dateSort: DateSort.CreatedDescending,
  pagination: { limit: 25 },
});

// Fetch next page
if (cursor) {
  const { records: next } = await tasks.records.query('task', {
    pagination: { limit: 25, cursor },
  });
}

read

Read a specific record by ID:

const { record } = await tasks.records.read('task', {
  filter: { recordId: taskId },
});

const data = await record.data.json(); // TaskData

Read from a remote DWN:

const { record } = await tasks.records.read('task', {
  from: 'did:dht:alice...',
  filter: { recordId: taskId },
});

delete

Delete via the TypedEnbox API:

const { status } = await tasks.records.delete('task', {
  recordId: record.id,
});

Or delete via the record instance:

await record.delete();

subscribe

Subscribe to real-time changes:

const { liveQuery } = await tasks.records.subscribe('task');

See the LiveQuery section below for event handling.

TypedRecord

Every record returned by TypedEnbox is wrapped in TypedRecord<T>, which provides type-safe data access and lifecycle methods.

Data accessors

const task = await record.data.json();    // Promise<TaskData>
const raw  = await record.data.text();    // Promise<string>
const blob = await record.data.blob();    // Promise<Blob>
const bytes = await record.data.bytes();  // Promise<Uint8Array>
const stream = await record.data.stream(); // Promise<ReadableStream>

Data is cached in memory up to 10 MB — subsequent calls to any accessor return the cached value without re-reading from the DWN.

Metadata

PropertyTypeDescription
record.idstringUnique, stable record ID
record.contextIdstring?Hierarchical grouping ID
record.dateCreatedstringISO 8601 creation timestamp (immutable)
record.timestampstringISO 8601 timestamp of last mutation
record.authorstringDID of the current message signer
record.creatorstringDID of the original author
record.protocolstring?Protocol URI
record.protocolPathstring?Path within the protocol structure
record.schemastring?Schema URI from the protocol type
record.dataFormatstring?MIME type of the payload
record.dataSizenumber?Payload size in bytes
record.tagsobject?Indexed key-value metadata
record.publishedboolean?Whether publicly readable
record.deletedbooleanWhether the record has been deleted
record.parentIdstring?Parent record ID
record.recipientstring?Recipient DID

Mutation methods

update

const { status, record: updated } = await record.update({
  data: { title: 'Buy oat milk', done: false },  // Partial<TaskData>
  tags: { priority: 'high' },
});

The data field accepts Partial<T> — only supply the fields you want to change. Both the returned record and the original instance reflect the updated state.

delete

const { status } = await record.delete();
// status.code: 202 on success

Delete with options:

await record.delete({ prune: true });  // also delete child records

Lifecycle methods

MethodDescription
record.send(target?)Push the record to a remote DWN. Defaults to your own remote DWN.
record.store(importRecord?)Persist to local DWN (for records created with store: false)
record.import(store?)Re-sign under your identity and optionally store (for importing others' records)
record.rawRecordEscape hatch to the underlying untyped Record

LiveQuery

LiveQuery (and its typed wrapper TypedLiveQuery<T>) provides an initial snapshot plus a real-time stream of deduplicated change events.

const { liveQuery } = await tasks.records.subscribe('task');

// Initial snapshot
for (const record of liveQuery.records) {
  console.log(await record.data.json());
}

// Real-time events
liveQuery.on('create', (record) => {
  console.log('New task:', record.id);
});

liveQuery.on('update', (record) => {
  console.log('Updated:', record.id);
});

liveQuery.on('delete', (record) => {
  console.log('Deleted:', record.id);
});

// Catch-all
liveQuery.on('change', (change) => {
  console.log(change.type, change.record.id);
});

Events

EventCallback argumentDescription
createTypedRecord<T>A new record was written
updateTypedRecord<T>An existing record was updated
deleteTypedRecord<T>A record was deleted
change{ type, record }Catch-all for any of the above
disconnected(none)Transport connection lost
reconnecting{ attempt: number }Reconnection attempt in progress
reconnected(none)Connection restored
eose(none)End of stored events — catch-up complete, events are now live

The .on() method returns an unsubscribe function:

const off = liveQuery.on('create', handler);
off(); // stop listening

Close the subscription when done:

await liveQuery.close();

Connection lifecycle

liveQuery.isConnected reflects the current transport state. Use the lifecycle events to show connection status in your UI:

liveQuery.on('disconnected', () => showOfflineBanner());
liveQuery.on('reconnecting', ({ attempt }) => {
  console.log(`Reconnection attempt ${attempt}...`);
});
liveQuery.on('reconnected', () => hideOfflineBanner());

Anonymous queries

Enbox.anonymous() creates a lightweight, read-only instance for querying published records without authentication:

const { dwn } = Enbox.anonymous();

const { records } = await dwn.records.query({
  from: 'did:dht:alice...',
  filter: {
    protocol: 'https://social.example/posts',
    protocolPath: 'post',
  },
});

for (const record of records) {
  console.log(record.id, await record.data.text());
}

No identity, vault, password, or signing keys are required. Only unsigned (anonymous) DWN messages are supported — you can query and read, but not write.

Hierarchical records

Protocols can define nested structures. Use parentContextId to create child records:

const NotebookProtocol = defineProtocol({
  protocol:  'https://example.com/notebooks',
  published: true,
  types: {
    notebook: {
      schema:      'https://example.com/schemas/notebook',
      dataFormats: ['application/json'],
    },
    page: {
      schema:      'https://example.com/schemas/page',
      dataFormats: ['application/json'],
    },
  },
  structure: {
    notebook: {
      page: {},
    },
  },
}, {} as {
  notebook: { name: string };
  page: { title: string; body: string };
});

const notebooks = enbox.using(NotebookProtocol);
await notebooks.configure();

// Create a parent
const { record: notebook } = await notebooks.records.create('notebook', {
  data: { name: 'Travel Notes' },
});

// Create a child under the parent
const { record: page } = await notebooks.records.create('notebook/page', {
  data: { title: 'Day 1', body: 'Arrived in Tokyo...' },
  parentContextId: notebook.contextId,
});

// Query children of a specific parent
const { records: pages } = await notebooks.records.query('notebook/page', {
  filter: { parentId: notebook.contextId },
});

Disconnecting

When your application shuts down or the user signs out, call disconnect() to stop background sync and release resources:

await enbox.disconnect();

After calling disconnect(), the Enbox instance should not be reused. Create a new one with Enbox.connect() for the next session.

On this page