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.