Skip to Content
AdvancedReal-time bindings

Real-time bindings

Versions, comments, audit log, and presence are bindings: you plug in adapters that talk to your storage + realtime backend. The simplest option is to use @useblok/sdk — it provides ready-made bindings for any BlokStorageAdapter.

Using the SDK

import { Blok } from "@useblok/core"; import { useBlokDocument, createMemoryAdapter } from "@useblok/sdk"; const adapter = createMemoryAdapter(); function EditorPage() { const doc = useBlokDocument({ adapter, documentId: "landing-home" }); if (doc.loading) return <Spinner />; return ( <Blok config={config} data={doc.initialData} versions={doc.versions} comments={doc.comments} presence={doc.presence} audit={doc.audit} commentAuthor={currentUser} onSave={doc.save} onPublish={doc.publish} /> ); }

useBlokDocument returns pre-wired bindings for every collaboration feature — you just drop them into <Blok> and everything works.

Storage adapters

A storage adapter is a small interface the SDK uses to persist everything. The SDK ships:

  • createMemoryAdapter() — in-memory (for demos / tests).
  • createIndexedDbAdapter() — browser IndexedDB (for offline-first).
  • createRestAdapter({ baseUrl }) — REST shape you implement server-side.

Or roll your own:

interface BlokStorageAdapter { loadDocument(id: string): Promise<Data | null>; saveDraft(id: string, data: Data): Promise<void>; publish(id: string, data: Data): Promise<void>; listVersions(id: string): Promise<DocumentVersion[]>; saveVersion(id: string, data: Data, meta: CreateVersionMeta): Promise<DocumentVersion>; restoreVersion(id: string, versionId: string): Promise<Data>; listComments(id: string): Promise<Comment[]>; createComment(id: string, input: CreateCommentInput): Promise<Comment>; // ... and so on for audit, presence }

Implement it once, and every collaboration feature works with your backend.

Without the SDK — raw bindings

If you don’t want the SDK, pass bindings directly. Each one is an object with a few async methods:

VersionsBinding

interface VersionsBinding { list(): Promise<DocumentVersionRef[]>; get(id: string): Promise<DocumentVersion | null>; create(data: Data, meta?: CreateVersionMeta): Promise<DocumentVersion>; remove(id: string): Promise<void>; subscribe?(cb: (ev: VersionChangeEvent) => void): () => void; }

CommentsBinding

interface CommentsBinding { list(): Promise<Comment[]>; create(input: CreateCommentInput): Promise<Comment>; update(id: string, patch: UpdateCommentPatch): Promise<Comment>; remove(id: string): Promise<void>; subscribe?(cb: (ev: CommentChangeEvent) => void): () => void; }

AuditBinding

interface AuditBinding { list(options?: AuditLogListOptions): Promise<AuditEvent[]>; append(input: AuditAppendInput): Promise<AuditEvent>; subscribe?(cb: (ev: AuditChangeEvent) => void): () => void; }

PresenceBinding

interface PresenceBinding { join(identity: PresenceIdentity): () => void; // returns a leave() fn announceFocus(focus: PresenceFocus | null): void; subscribe(cb: (ev: PresenceEvent) => void): () => void; }

subscribe is how realtime works

If your binding implements subscribe, Blok will re-render automatically when the subscription emits. That’s how two editors see each other’s new comments / versions / audit events in real time.

Typical transports:

  • BroadcastChannel — works across tabs on the same origin, no server needed.
  • SSE — server push, one-way, simple.
  • WebSocket — bidirectional, best for presence.
  • Yjs / Automerge — full CRDT co-editing (not built in — you’d wire it through the store).

Auth + identity

Pass commentAuthor, auditActor, and presenceIdentity to <Blok> to stamp every event with the current user:

<Blok commentAuthor={{ id: user.id, name: user.name, avatarUrl: user.avatar }} // auditActor defaults to commentAuthor // presenceIdentity defaults to commentAuthor />

All identity props are UI-only. Your backend must verify identity from its own auth layer — don’t trust author.id from the client.

Last updated on