If you want to update this page or add new content, please submit a pull request to the Homepage.

JS SDK

The Yorkie JS SDK enables you to efficiently build collaborative applications. On the client side, you can create documents that automatically sync with remote peers with minimal effort.

If you want to install the SDK, refer to the Getting Started with JS SDK.

Client

Client is a normal client that communicates with the server. It holds documents and sends local changes to the server to synchronize with other replicas.

Creating a Client

Create a Client using new yorkie.Client(). After activation, the Client connects to the server and is ready to use.

1const client = new yorkie.Client({
2 rpcAddr: 'https://api.yorkie.dev',
3 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
4});
5await client.activate();

The API key is used to identify the project in Yorkie. You can get the API key of the project you created in the Dashboard.

Auth Token

Configure authentication for the client by setting up an authTokenInjector to provide tokens for Auth Webhook verification. If a codes.Unauthenticated error occurs, the authTokenInjector is called again with the webhook's response reason, enabling automatic token refresh. For more information about Auth Webhook, please refer to the Auth Webhook.

1const client = new yorkie.Client({
2 rpcAddr: 'https://api.yorkie.dev',
3 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
4 authTokenInjector: async (reason) => {
5 // Handle token refresh logic based on webhook response
6 if (reason === 'token expired') {
7 return await refreshAccessToken();
8 }
9 return accessToken;
10 },
11});

Measuring MAU

Measure MAU (Monthly Active Users) by setting the userID metadata when creating a client. The userID should be a unique identifier for each user.

1const client = new yorkie.Client({
2 rpcAddr: 'https://api.yorkie.dev',
3 apiKey: 'xxxxxxxxxxxxxxxxxxxx',
4 metadata: { userID: 'user-1234' },
5});

You can check the MAU in the Dashboard by selecting the project and clicking the overview tab.

Deactivate the Client

When the client is no longer needed, you can deactivate it to release resources and disconnect from the server.

1await client.deactivate();

This will detach all documents attached to the client for efficient garbage collection.

Document

Document is the primary data type in Yorkie, providing a JSON-like updating experience for representing your application's model. Documents can be updated without being attached to the client, and changes automatically propagate to other clients when attached or when the network is restored.

Creating a Document

Create a Document with a unique key:

1const doc = new yorkie.Document('doc-1');

Document keys can contain a-z, A-Z, 0-9, -, ., _, ~ and must be less than 120 characters.

Attaching the Document

Attaching subscribes the client to a document. If the document doesn't exist on the server, it is created. Once attached, all modifications automatically synchronize with other clients.

Attach options:

  • initialPresence: Sets the client's initial presence when attaching to the document. The presence is shared with other users participating in the document. It must be serializable to JSON.
  • syncMode(Optional): Specifies synchronization modes. The default value is SyncMode.Realtime, which automatically pushes and pulls changes. If you set it to SyncMode.Manual, you'll need to manually handle synchronization.
1await client.attach(doc, {
2 initialPresence: { color: 'blue', cursor: { x: 0, y: 0 } },
3 syncMode: SyncMode.Manual,
4});

Initializing root

Set initial values for the document root when attaching:

1await client.attach(doc, {
2 initialRoot: {
3 list: [1, 2, 3],
4 counter: new yorkie.Counter(yorkie.IntType, 0),
5 },
6});

The initial values are partially applied. For each element in initialRoot:

  • If the key doesn't exist, the element is applied.
  • If the key already exists in the document, that element is discarded. You don't need to worry about overwriting existing values.
1await client.attach(doc, {
2 initialRoot: {
3 list: [],
4 },
5});
6
7// Another client tries to attach with initialRoot option:
8await client.attach(doc, {
9 initialRoot: {
10 list: [1, 2, 3], // this update will be discarded
11 counter: new yorkie.Counter(yorkie.IntType, 0), // this update will be applied
12 },
13});
14
15// final state
16// root = {
17// list: [],
18// counter: {}
19// }

We support element types for Primitives, and Custom CRDT types.

Elements added by initialRoot are applied locally after push-pull during attach, not sent to the server immediately.

Editing the Document

Use Document.update(changeFn, message) to modify a Document. The optional message parameter adds a description to the change.

1const message = 'update document for test';
2doc.update((root) => {
3 root.todos = [];
4 root.todos.push('todo-1');
5 root.obj = {
6 name: 'yorkie',
7 age: 14,
8 };
9 root.counter = new yorkie.Counter(yorkie.IntType, 0);
10 root.counter.increase(1);
11}, message);

Under the hood, root in the update function creates a change, a set of operations, using a JavaScript proxy. Every element has its unique ID, created by the logical clock. This ID is used by Yorkie to track which object is which.

You can get the contents of the Document using document.getRoot().

1const root = doc.getRoot();
2console.log(root.todos); // ["todo-1"]
3console.log(root.obj); // {"name":"yorkie","age":14}
4console.log(root.obj.name); // "yorkie"
5console.log(root.counter.getValue()); // 1

Plain JavaScript Objects

Convert Yorkie's JSONObject types to plain JavaScript objects using toJS():

1const root = doc.getRoot();
2
3// Converting the entire root to a plain object
4const plainObject = root.toJS();
5console.log(plainObject); // Plain JavaScript object
6
7// Converting specific nested objects
8const plainTodos = root.todos.toJS();
9const plainObj = root.obj.toJS();

The toJS() method creates a deep copy as a plain JavaScript object, removing all Yorkie-specific metadata and proxies.

Detaching the Document

Detach documents when no longer needed to improve garbage collection efficiency:

1await client.detach(doc);

The Remove on Detach project setting enforces server-side removal when the last client detaches from a document.

Presence

Presence tracks and shares user state (e.g., cursor positions, selections) in real-time.

Updating presence

Update the current user's presence using Document.update(). Properties are merged, and unspecified properties remain unchanged.

1doc.update((root, presence) => {
2 presence.set({ cursor: { x: 1, y: 1 } });
3});
4
5// final state
6// presence = { color: 'blue', cursor: { x: 1, y: 1 } }
7// we can see that the changes made were merged and the final state of the current user's presence is as we desire

Note, the properties provided will be replaced entirely and not merely updated.

For example:

1await client.attach(doc, {
2 // let's say 'color' is a property of cursor
3 initialPresence: {cursor: { x: 0, y: 0, color: 'red'}},
4});
5
6doc.update((root, presence) => {
7 // we want to change the x y coordinates of our cursor
8 presence.set({ cursor: { x: 1, y: 1 } });
9});
10
11// final state
12// presence = { cursor: { x: 1, y: 1 } }
13// we can see that all properties inside cursor get replaced (i.e. we lose the property 'color')

Getting presence

Document.getPresence(clientID) returns the presence of a specific client:

1doc.getPresence(client.getID()); // { color: 'blue', cursor: { x: 1, y: 1 } }

Document.getMyPresence() returns the presence of the current client:

1doc.getMyPresence(); // { color: 'blue', cursor: { x: 1, y: 1 } }

Document.getPresences() returns all participating clients with their presence:

1const users = doc.getPresences();
2for (const { clientID, presence } of users ) {
3 // Do something
4}

Example: Profile Stack

Subscribing to presence changes

Document.subscribe('presence') notifies you when clients watch, unwatch, or modify their presence.

The initialized event occurs when connecting, losing connection, or reconnecting to the watch stream.

Subscribe before attaching the document to ensure you receive the initial initialized event.

1const unsubscribe = doc.subscribe('presence', (event) => {
2 if (event.type === 'initialized') {
3 // event.value: Array of users currently participating in the document
4 }
5
6 if (event.type === 'watched') {
7 // event.value: A user has joined the document editing in online
8 }
9
10 if (event.type === 'unwatched') {
11 // event.value: A user has left the document editing
12 }
13
14 if (event.type === 'presence-changed') {
15 // event.value: A user has updated their presence
16 }
17});

Use my-presence and others topics to distinguish between your own events and those of others.

Document.subscribe('my-presence') subscribes to the current client's presence changes.

Possible event types: initialized, presence-changed.

1const unsubscribe = doc.subscribe('my-presence', (event) => {
2 // Do something
3});

Document.subscribe('others') subscribes to other clients' presence changes.

Possible event types: watched, unwatched, presence-changed.

1const unsubscribe = doc.subscribe('others', (event) => {
2 if (event.type === 'watched') {
3 addUser(event.value);
4 }
5
6 if (event.type === 'unwatched') {
7 removeUser(event.value);
8 }
9
10 if (event.type === 'presence-changed') {
11 updateUser(event.value);
12 }
13});

Synchronization Modes

Change the synchronization mode using client.changeSyncMode(doc, syncMode):

Available modes:

  • SyncMode.Realtime: Local changes are automatically pushed to the server, and remote changes are pulled from the server.

  • SyncMode.RealtimePushOnly: Only local changes are pushed, and remote changes are not pulled.

  • SyncMode.RealtimeSyncOff: Changes are not synchronized, but the watch stream remains active.

  • SyncMode.Manual: Synchronization no longer occurs in real-time, and the watch stream is disconnected. Manual handling is required for synchronization.

1// Enable automatic synchronization of both local and remote changes.
2await client.changeSyncMode(doc, SyncMode.Realtime);
3
4// Only push local changes automatically.
5await client.changeSyncMode(doc, SyncMode.RealtimePushOnly);
6
7// Synchronization turned off, but the watch stream remains active.
8await client.changeSyncMode(doc, SyncMode.RealtimeSyncOff);
9
10// Synchronization turned off, and the watch stream is disconnected.
11await client.changeSyncMode(doc, SyncMode.Manual);
12await client.sync(doc); // Trigger synchronization manually using the sync function.

Subscribing to Changes

Subscribe to document events including changes, connection status, and synchronization status.

Document.subscribe()

Subscribe to document modification events. The event.type can be local-change, remote-change, or snapshot:

1const unsubscribe = doc.subscribe((event) => {
2 if (event.type === 'snapshot') {
3 // `snapshot` delivered when the entire document is updated from the server.
4 } else if (event.type === 'local-change') {
5 // `local-change` delivered when calling document.update from the current client.
6 } else if (event.type === 'remote-change') {
7 // `remote-change` delivered when the document is updated from other clients.
8 const { message, operations } = event.value;
9
10 // You can access the operations that have been applied to the document.
11 for (const op of operations) {
12 // e.g.) { type: 'increase', value: 1, path: '$.counter' }
13 switch (op.type) {
14 case 'increase':
15 // ...
16 break;
17 }
18 }
19 }
20});

When the event.type is local-change or remote-change, the event.value is a changeInfo, which has {operations, message} properties. For more information about changeInfo for document events, please refer to the ChangeInfo.

The event.rawChange value for local-change and remote-change events, and the event.value.snapshot for snapshot event, are set only when enableDevtools option is configured as true.

The snapshot event occurs when the number of changes to fetch from the server exceeds SnapshotThreshold. The server sends a complete snapshot instead of individual changes. See this example code for handling snapshots in CodeMirror.

Clients that haven't synchronized for a long time may receive a snapshot event. Ensure that your application handles these events to maintain synchronization.

Document.subscribe('$.path')

Subscribe to changes for a specific path (e.g., $.todos):

1// The event is triggered when the value of the path("$.todos") is changed.
2const unsubscribe = doc.subscribe('$.todos', (event) => {
3 // You can access the updated value of the path.
4 const target = doc.getValueByPath('$.todos');
5});

Document.subscribe('connection')

Monitor the watch stream connection status. Possible values are StreamConnectionStatus.Connected and StreamConnectionStatus.Disconnected.

1const unsubscribe = doc.subscribe('connection', (event) => {
2 if (event.value === StreamConnectionStatus.Connected) {
3 // The watch stream is connected.
4 } else if (event.value === StreamConnectionStatus.Disconnected) {
5 // The watch stream is disconnected.
6 }
7});

For more information about StreamConnectionStatus, please refer to the StreamConnectionStatus.

Document.subscribe('sync')

Track synchronization status in SyncMode.Realtime. Possible values are DocSyncStatus.Synced and DocSyncStatus.SyncFailed.

1const unsubscribe = doc.subscribe('sync', (event) => {
2 if (event.value === DocSyncStatus.Synced) {
3 // The document is synchronized with the server.
4 } else if (event.value === DocSyncStatus.SyncFailed) {
5 // The document failed to synchronize with the server.
6 }
7});

For more information about DocSyncStatus, please refer to the DocSyncStatus.

Document.subscribe('status')

Subscribe to document status changes. Possible values are DocStatus.Attached, DocStatus.Detached, and DocStatus.Removed.

1const unsubscribe = doc.subscribe('status', (event) => {
2 if (event.value.status === DocStatus.Attached) {
3 // The document is attached to the client.
4 } else if (event.value.status === DocStatus.Detached) {
5 // The document is detached from the client.
6 } else if (event.value.status === DocStatus.Removed) {
7 // The document is removed and cannot be edited
8 }
9});

For more information about DocStatus, please refer to the DocStatus.

In web applications, detecting browser closure or navigation is difficult. Yorkie's client-deactivate-threshold automatically deactivates inactive clients, triggering a DocumentStatus.Detached event. See Client Deactivate Threshold.

Document.subscribe('auth-error')

Subscribe to authentication errors during PushPull or WatchDocuments operations:

1const unsubscribe = doc.subscribe('auth-error', (event) => {
2 console.log(event.value);
3 // event.value contains:
4 // - reason: string
5 // - method: 'PushPull' | 'WatchDocuments'
6});

This subscription allows you to monitor when token refreshes occur due to authentication errors.
For more information about Auth Webhook, please refer to the Auth Webhook.

Document.subscribe('all')

Subscribe to all document events. Used for Devtools extension.

1const unsubscribe = doc.subscribe('all', (transactionEvent) => {
2 for (const docEvent of transactionEvent) {
3 console.log(docEvent);
4 }
5});

Custom CRDT Types

Specialized data types for collaborative applications. Created in document.update() callbacks.

Text

Supports collaborative text editing with styling attributes:

1doc.update((root) => {
2 root.text = new yorkie.Text(); // {"text":""}
3 root.text.edit(0, 0, 'hello'); // {"text":"hello"}
4 root.text.edit(0, 1, 'H'); // {"text":"Hello"}
5 root.text.setStyle(0, 1, { bold: true }); // {"text":"<b>H</b>ello"}
6});

Selection using presence

Share text selection using presence instead of storing it permanently. Convert index to position for Yorkie.Text.

1// Update selection through text editing
2codemirror.on('beforeChange', (cm, change) => {
3 doc.update((root, presence) => {
4 const range = root.content.edit(from, to, content); // return updated index range
5 presence.set({
6 selection: root.content.indexRangeToPosRange(range), // update presence
7 });
8 });
9});
10
11// Update selection using mouse or keyboard
12codemirror.on('beforeSelectionChange', (cm, change) => {
13 const fromIdx = cm.indexFromPos(change.ranges[0].anchor);
14 const toIdx = cm.indexFromPos(change.ranges[0].head);
15 doc.update((root, presence) => {
16 presence.set({
17 selection: root.content.indexRangeToPosRange([fromIdx, toIdx]), // update presence
18 });
19 });
20});
  • When applying other user's selection changes:
1doc.subscribe('others', (event) => {
2 if (event.type === 'presence-changed') {
3 const { clientID, presence } = event.value;
4 const range = doc.getRoot().content.posRangeToIndexRange(presence.selection);
5 // Handle the updated selection in the editor
6 }
7});

Examples:

Counter

Supports integer operations with concurrent modifications:

1doc.update((root) => {
2 root.counter = new yorkie.Counter(yorkie.IntType, 1); // {"counter":1}
3 root.counter.increase(2); // {"counter":3}
4 root.counter.increase(3); // {"counter":6}
5 root.counter.increase(-4); // {"counter":2}
6});

TypeScript Support

For stricter type checking, you can use type variables in TypeScript when creating a Document.

1import yorkie, { JSONArray } from '@yorkie-js/sdk';
2type DocType = {
3 list: JSONArray<number>;
4 text: yorkie.Text;
5};
6type PresenceType = {
7 username: string;
8 color: string;
9};
10
11const doc = new yorkie.Document<DocType, PresenceType>('key');
12await client.attach(doc, {
13 initialPresence: {
14 username: 'alice',
15 color: 'blue',
16 },
17});
18doc.update((root, presence) => {
19 root.list = [1, 2, 3];
20 root.text = new yorkie.Text();
21 presence.set({ color: 'red' });
22});

Document Limits

Yorkie enforces the following limits for system stability and performance. Configure these in the Dashboard's Project Settings.

LimitDescriptionDefault Behavior
Max AttachmentsMaximum clients that can attach to a document simultaneouslyFailed attachments require manual retry
Max SubscribersMaximum clients that can maintain subscription streamsSDK auto-retries (1/sec) when limit reached
Max Document SizeMaximum allowed document size on attachmentLocal updates exceeding limit are rejected

Understanding Document Size

A document's size includes both visible content and CRDT metadata required for synchronization. The actual size may exceed what you see in the editor.

  • Local updates that exceed the size limit are rejected and not pushed to the server
  • Remote updates are always applied to ensure state convergence across clients

Tip: Monitor connection status using Document.subscribe('connection') to detect when limits are reached.

Document Revisions

Save snapshots of your document at specific points in time, browse history, and restore previous versions.

Creating a Revision

Create a revision using client.createRevision():

1// Create a revision with a label
2const revision = await client.createRevision(doc, 'v1.0');
3
4// Create a revision with a label and description
5const revision = await client.createRevision(
6 doc,
7 'Feature complete',
8 'Added user authentication and profile management'
9);

Parameters:

  • doc: The document to create a revision for
  • label: A short, descriptive label for the revision (required)
  • description (Optional): A detailed description of what changed in this revision

Revisions are stored on the server and can be accessed by any client with access to the document. They persist independently of the document's current state.

Listing Revisions

List all revisions using client.listRevisions():

1// Get the first 50 revisions (newest first)
2const revisions = await client.listRevisions(doc);
3
4// Get revisions with pagination
5const revisions = await client.listRevisions(doc, {
6 pageSize: 20,
7 offset: 0,
8 isForward: false, // false = newest first, true = oldest first
9});
10
11// Display revision information
12revisions.forEach(revision => {
13 console.log(`ID: ${revision.id}`);
14 console.log(`Label: ${revision.label}`);
15 console.log(`Description: ${revision.description}`);
16 console.log(`Created at: ${revision.createdAt}`);
17 console.log(`Server Sequence: ${revision.serverSeq}`);
18});

Available options:

  • pageSize (Optional, default: 50): Number of revisions to retrieve per request
  • offset (Optional, default: 0): Number of revisions to skip for pagination
  • isForward (Optional, default: false): Sort order - false for newest first, true for oldest first

Each revision summary includes:

  • id: Unique identifier for the revision
  • label: The label provided when creating the revision
  • description: Optional description of the revision
  • createdAt: Timestamp when the revision was created
  • serverSeq: Server sequence number at the time of revision creation

Getting Revision Details

Get detailed information using client.getRevision():

1// Get a specific revision by ID
2const revision = await client.getRevision(doc, revisionId);
3
4// Access the revision's snapshot, YSON representation
5const snapshot = revision.snapshot;
6console.log('Revision data:', snapshot);

The returned revision object contains:

  • All metadata from the revision summary
  • snapshot: document state at the time of the revision, YSON representation

The snapshot contains the full document data as it existed when the revision was created. This data can be used for comparison, preview, or restoration purposes.

Parsing YSON Snapshots

Revision snapshots are in YSON (Yorkie JSON) format. Use the YSON parser to convert them to usable data structures:

1import { YSON } from '@yorkie-js/sdk';
2
3// Get a revision with its snapshot
4const revision = await client.getRevision(doc, revisionId);
5
6if (revision.snapshot) {
7 try {
8 // Parse the YSON snapshot into Yorkie's internal data structure
9 const root = YSON.parse(revision.snapshot);
10
11 // Access the parsed data
12 console.log(root.title); // Access primitive values
13 console.log(root.todos); // Access arrays
14 console.log(root.obj.name); // Access nested objects
15 } catch (error) {
16 console.error('Failed to parse YSON:', error);
17 }
18}

Parsing with TypeScript types:

You can provide type information for better type safety when parsing YSON snapshots:

1import { YSON } from '@yorkie-js/sdk';
2
3type DocType = {
4 title: string;
5 content: YSON.Text;
6 todos: Array<string>;
7};
8
9const revision = await client.getRevision(doc, revisionId);
10
11if (revision.snapshot) {
12 const root = YSON.parse<DocType>(revision.snapshot);
13 // root is now typed as DocType
14 console.log(root.title);
15}

Converting Text to String:

When working with Yorkie's Text type in snapshots, you can use YSON.textToString() to convert it to a plain string:

1import { YSON } from '@yorkie-js/sdk';
2
3const revision = await client.getRevision(doc, revisionId);
4
5if (revision.snapshot) {
6 const root = YSON.parse<{ content: YSON.Text }>(revision.snapshot);
7
8 // Convert Yorkie Text to plain string
9 const plainText = YSON.textToString(root.content);
10 console.log(plainText);
11
12 // You can now use this string in your application
13 // For example, display it in a text editor or compare with other versions
14}

The YSON parser is designed to handle Yorkie's internal data structures. If you're working with standard Document data, you may also consider using the toJS() method on the live document for a simpler conversion to plain JavaScript objects.

Restoring a Revision

Restore a document to a previous revision using client.restoreRevision():

1// Restore to a specific revision
2await client.restoreRevision(doc, revisionId);
3
4// Sync to ensure the restored state is propagated
5await client.sync();

Restoring replaces the entire document state and cannot be undone automatically. Consider creating a new revision before restoring to preserve the current state.

Example workflow:

1// Create a revision
2const v1 = await client.createRevision(doc, 'v1.0', 'Initial version');
3
4// List all revisions
5const revisions = await client.listRevisions(doc);
6
7// Get revision details with snapshot
8const revision = await client.getRevision(doc, v1.id);
9
10// Restore to a previous revision
11await client.restoreRevision(doc, v1.id);
12await client.sync();

History (Undo/Redo)

Built-in undo/redo for all document changes.

Basic Usage

1doc.update((root) => {
2 root.text = new yorkie.Text();
3 root.text.edit(0, 0, 'Hello');
4});
5
6// Undo the last change
7doc.history.undo();
8console.log(doc.getRoot().text.toString()); // ""
9
10// Redo the undone change
11doc.history.redo();
12console.log(doc.getRoot().text.toString()); // "Hello"

Checking Availability

1doc.update((root) => {
2 root.counter = new yorkie.Counter(yorkie.IntType, 0);
3 root.counter.increase(5);
4});
5
6console.log(doc.history.canUndo()); // true
7console.log(doc.history.canRedo()); // false
8
9doc.history.undo();
10console.log(doc.history.canUndo()); // false
11console.log(doc.history.canRedo()); // true

Redo Stack Behavior

New changes after undo clear the redo stack:

1doc.update((root) => {
2 root.value = 1;
3});
4
5doc.update((root) => {
6 root.value = 2;
7});
8
9doc.history.undo();
10console.log(doc.history.canRedo()); // true
11
12// Making a new change clears the redo stack
13doc.update((root) => {
14 root.value = 3;
15});
16console.log(doc.history.canRedo()); // false

Supported Operations

History supports all CRDT types and operations including:

  • Text: edit operations (setStyle support is under development)
  • Object: property set, delete operations
  • Array: push, insert, delete, move operations
  • Counter: increase operations
  • Tree: Undo/Redo support is under development
1doc.update((root) => {
2 root.todos = [];
3 root.todos.push('Task 1');
4 root.todos.push('Task 2');
5});
6
7doc.history.undo(); // Undoes 'Task 2' push
8console.log(doc.getRoot().todos.toJS()); // ['Task 1']
9
10doc.history.redo(); // Redoes 'Task 2' push
11console.log(doc.getRoot().todos.toJS()); // ['Task 1', 'Task 2']

Limitations

  • Maximum Stack Depth: 50 changes per stack.
  • Not Available During Update: You cannot call undo() or redo() inside a doc.update() callback.
1doc.update((root) => {
2 doc.history.undo(); // ❌ Throws Error: "Undo is not allowed during an update"
3});
  • Local Only: History tracks only local changes. Remote changes are applied but not added to undo/redo stacks.

Undo/redo operations only affect your own editing history and seamlessly integrate with changes from other users.

Examples

For complete working examples of history implementation, see:

Channel

Channel is a communication channel that enables real-time pub/sub messaging and presence tracking between clients. Unlike Documents, which persist and synchronize data, Channels are designed for ephemeral, real-time communication such as chat messages, notifications, and presence indicators.

Creating a Channel

Create a Channel with a unique identifier and attach it to the Client:

1const channel = new yorkie.Channel('room-123');
2await client.attach(channel);

The channel key is used to identify the Channel in Yorkie. Clients with the same channel key can communicate with each other through pub/sub messaging.

Hierarchical Channel Keys

Channels support a hierarchical structure using periods (.) as separators, allowing you to organize channels into logical groups and subgroups. This structure is particularly useful for managing complex applications with multiple rooms, sections, or nested contexts.

1// Create channels with hierarchical keys
2const roomChannel = new yorkie.Channel('room-1');
3const sectionChannel = new yorkie.Channel('room-1.section-a');
4const userChannel = new yorkie.Channel('room-1.section-a.user-123');
5
6await client.attach(roomChannel);
7await client.attach(sectionChannel);
8await client.attach(userChannel);

Each level in the hierarchy maintains its own presence count and can be used independently:

  • room-1 might track all users in a game room
  • room-1.section-a might track users in a specific section of that room
  • room-1.section-a.user-123 might track presence for a specific user's context

Channel Key Restrictions:

When using hierarchical channel keys, follow these rules:

RuleInvalid ExamplesValid Examples
Cannot start with a period.room-1
.room-1.section-1
room-1
room-1.section-1
Cannot end with a periodroom-1.
room-1.section-1.
room-1
room-1.section-1
Cannot contain consecutive periodsroom-1..section-1
room..section..subsection
room-1.section-1
room.section.subsection

Query channel information: Use the GetChannels API to retrieve presence counts for specific channels and their sub-levels.

Important: Distribute First-Level Channel Keys

Yorkie servers determine which server handles a channel based on a combination of your project and the first-level key of the channel. Using the same first-level key for all channels means that a single server manages all your channels, which can lead to performance bottlenecks and uneven load distribution.

Design your channel key structure to naturally distribute traffic across different first-level prefixes for optimal performance.

1// ✅ Good: Distributed first-level keys
2new yorkie.Channel('game-1.room-a');
3new yorkie.Channel('chat-1.thread-1');
4new yorkie.Channel('chat-2.thread-1');
5new yorkie.Channel('notification-1.user-123');
6
7// ❌ Bad: Same first-level key
8// All channels share 'app' as first level - avoid this!
9new yorkie.Channel('app.game-1');
10new yorkie.Channel('app.game-2');
11new yorkie.Channel('app.chat-1');
12new yorkie.Channel('app.notification-1');

Broadcast

Channels support a pub/sub pattern for broadcasting ephemeral messages to all connected clients.

1// Subscribe to messages on a specific topic
2const unsubscribe = channel.subscribe('chat', (event) => {
3 console.log(`[${event.topic}] ${event.payload.message}`);
4 console.log(`From: ${event.publisher}`);
5});
6
7// Broadcast a message to all clients in the same channel
8await channel.broadcast('chat', { message: 'Hello, world!' });

The event object contains:

  • topic: The topic name you subscribed to
  • payload: The message data (must be JSON serializable)
  • publisher: The client ID of the message sender

You can subscribe to multiple topics on the same channel.

1// Subscribe to different topics
2channel.subscribe('chat', (event) => {
3 console.log('Chat message:', event.payload);
4});
5
6channel.subscribe('notification', (event) => {
7 console.log('Notification:', event.payload);
8});
9
10// Broadcast to different topics
11await channel.broadcast('chat', { message: 'Hello!' });
12await channel.broadcast('notification', { type: 'user-joined', user: 'Alice' });

Broadcast messages are ephemeral and only delivered to clients currently connected to the channel. Messages are not persisted or stored, so clients that join later will not receive previous messages.

Tracking Online Sessions

Channels automatically track the number of connected clients, making it easy to display session counters.

1// Subscribe to presence changes
2const unsubscribe = channel.subscribe('presence', (event) => {
3 console.log(`Sessions: ${event.count}`);
4});
5
6// Get the current session count
7const count = channel.getSessionCount();
8console.log(`Currently ${count} sessions`);

The session count is automatically updated when clients connect or disconnect from the channel.

Sessions are connection-based and managed for scalability. In abnormal cases (such as app crashes or network partitions), a client might not detach cleanly, so the server may keep the session counted until it is considered stale and cleaned up. This means the displayed count can be approximate and may lag briefly. Avoid using session counts as a source of truth for critical business logic such as billing, authorization, or settlements.

Detaching a Channel

When you're done with a channel, detach it to clean up resources and stop receiving messages:

1await client.detach(channel);

Use Cases

Channels are ideal for:

  • Chat applications: Real-time messaging without persistence
  • Live notifications: Temporary alerts and updates
  • Presence indicators: Showing who's online or in a room
  • Cursor sharing: Broadcasting cursor positions in collaborative tools
  • Event broadcasting: Sending signals or triggers between clients

For persistent data that needs to be synchronized and stored, use Document instead.

Logger Options

The Logger outputs SDK events to the console for debugging purposes. To modify these options, use the setLogLevel function.

1import { setLogLevel, LogLevel } from '@yorkie-js/sdk';
2
3setLogLevel(LogLevel.Error); // Display logs with Error or higher

The available log levels for setLogLevel are:

LogLevelDescription
LogLevel.TrivialMost verbose level, displays all logs
LogLevel.DebugDetailed information for debugging
LogLevel.InfoGeneral information
LogLevel.WarnWarnings and potential issues
LogLevel.ErrorErrors and unexpected behavior
LogLevel.FatalCritical errors, may lead to termination

Adjust the log level for flexible control over log verbosity in your application.

Reference

For details on how to use the JS SDK, please refer to JS SDK Reference.