decentrl.
SDK

Client API

The client instance provides methods for publishing events, managing contracts, querying state, and syncing.

The client is the primary interface for interacting with the Decentrl network. It's created from an app definition and handles all cryptographic operations internally.

const client = app.createClient({
  mediatorDid: 'did:web:mediator.decentrl.io',
})

Identity

The client manages your cryptographic identity through client.identity:

// Create a new identity (generates keys, DID, registers with mediator)
await client.identity.create({ alias: 'alice', mediatorDid: 'did:web:mediator.decentrl.io' })

// Access current identity
const identity = client.identity.getIdentity()
// { did, alias, mediatorDid, mediatorEndpoint, keys, mediatorContract }

// Serialize for persistence
const serialized = client.identity.serialize()

// Load a previously serialized identity
client.identity.load(serialized)

// Listen for identity changes
const unsub = client.identity.onChange((identity) => { ... })

Publishing Events

await client.publish('message', {
  text: 'Hello Bob',
  threadId: crypto.randomUUID(),
})

Under the hood, publish:

  1. Validates the payload against the Zod schema
  2. Evaluates tag templates from the event definition
  3. Signs the event envelope with Ed25519
  4. Encrypts with the contract root secret and sends to recipient's mediator
  5. Re-encrypts with storage key and saves on own mediator with encrypted tags
  6. Updates local state through the matching reducer

Options:

await client.publish('message', data, {
  recipient: bobDid,    // send to specific recipient
  ephemeral: true,      // don't store locally (typing indicators, presence)
})

Public Events

Publish signed, unencrypted events to a public channel:

// Publish a public event (validated against publicEvents schema)
const { publicEventId } = await client.publishPublic('blog.post', {
  title: 'Hello World',
  body: 'My first public post',
})

// Delete a public event
await client.deletePublicEvent(publicEventId, 'blog.post')

Under the hood, publishPublic:

  1. Validates the payload against the public event's Zod schema
  2. Evaluates tag templates from the definition
  3. Signs { channel_id, event, tags, timestamp } with Ed25519
  4. Sends a ONE_WAY_PUBLIC command to the mediator
  5. Updates local state through the matching public: reducer

Querying Public Events

Read public events from any identity — no contract or identity required:

// Query a publisher's public events
const result = await client.queryPublicEvents({
  publisherDid: 'did:decentrl:alice:...',
  channelId: 'blog',
  tags: ['featured'],
  pagination: { page: 0, pageSize: 20 },
})
// result.data — array of PublicEventResult
// result.pagination — { page, pageSize, total }

Each result includes the event content, tags, timestamp, and event signature for verification.

Subscribing to Channels

For continuous polling of an external identity's public channel:

const { unsubscribe } = client.subscribeToChannel({
  did: 'did:decentrl:alice:...',
  channelId: 'blog',
  pollIntervalMs: 10000,
})

// Events are automatically validated, signature-verified,
// and dispatched to channel: reducers

// Stop polling
unsubscribe()

Public Event Listeners

// Listen for public events (both own and channel)
const unsub = client.onPublicEvent((envelope) => {
  console.log(envelope.type, envelope.data, envelope.meta.publisherDid)
})

Contracts

Manage communication contracts through client.contracts:

// Send a contract request to another identity
await client.contracts.request(bobDid)

// Query pending inbound contract requests
const pending = await client.contracts.getPending()

// Accept a pending contract request
await client.contracts.accept(
  pending[0].id,
  pending[0].encryptedPayload,
  pending[0].requestorEphemeralPublicKey,
)

// Refresh contracts from mediator
await client.contracts.refresh()

// Get all active contracts
const contracts = client.contracts.getActiveContracts()

// Find a specific contract by DID
const contract = client.contracts.getContractByDid(bobDid)

// Listen for contract changes
const unsub = client.contracts.onChange(() => { ... })

State

Access reactive state derived from your reducers:

// Get current state snapshot
const state = client.getState()
// { messages: [...], reactions: [...], ... }

// Subscribe to state changes
const unsub = client.subscribe((state) => {
  console.log('Messages:', state.messages.length)
})

Querying Events

// Query stored events with filters
const result = await client.query({
  tags: ['thread.abc'],
  afterTimestamp: Date.now() / 1000 - 86400,
  pagination: { page: 0, pageSize: 50 },
})
// result.data — array of EventEnvelope
// result.pagination — { page, pageSize, total }

// Query and process events through reducers
const { processed, total } = await client.queryAndProcess({
  tags: ['thread.abc'],
})

// Listen for individual events
const unsub = client.onEvent((envelope) => {
  console.log('Event:', envelope.type, envelope.data)
})

Sync

Start automatic background synchronization:

client.startSync({
  intervalMs: 3000,           // poll interval
  websocket: true,            // enable WebSocket push
  autoRenew: {
    enabled: true,
    threshold: 0.2,           // renew when 20% of contract lifetime remains
  },
  onError: (err) => console.error('Sync error:', err),
})

// Manual sync
await client.sync()

// Stop sync
client.stopSync()

// Connection status
const status = client.getConnectionStatus()
// 'disconnected' | 'connecting' | 'authenticating' | 'connected'

const unsub = client.onConnectionStatusChange((status) => { ... })

Lifecycle

// Reset client state (identity, contracts, state)
client.reset()