Custom Backend
By default, Unlayer stores collaboration threads and comments for you. You can store them in your own database instead — to own and retain the data, for data residency, compliance, or integration reasons. Just register your own storage and the builder uses it in place of Unlayer's cloud, whether you use Unlayer's cloud or run on-premise.
- Providers load threads and comments (reads).
- Callbacks persist changes (writes).
When you register the collaborationThreads provider, the builder calls it instead of Unlayer's cloud storage — likewise for the write callbacks. This works with both Unlayer's cloud and on-premise setups.
unlayer.init({
id: 'editor',
projectId: 1234,
designId: 'design-5678',
user: { id: 76, name: 'Jane Doe' },
features: { collaboration: true },
});
When you use Unlayer's cloud, providers and callbacks override the cloud storage when present, and the builder falls back to the cloud when they're absent. In an on-premise setup (with no Unlayer cloud), they're required — the Comments button stays hidden until you register the collaborationThreads provider.
Providers
Register each provider with unlayer.registerProvider(name, handler). The handler receives params and a done function to call with your result.
| Provider | Params | Expected result |
|---|---|---|
collaborationThreads | { projectId, designId } | { success: true, data: Thread[] } |
collaborationThreadComments | { threadId } | { success: true, data: Comment[] } |
unlayer.registerProvider('collaborationThreads', async (params, done) => {
// params = { projectId, designId }
const threads = await fetchThreadsFromYourBackend(params);
done({ success: true, data: threads });
});
unlayer.registerProvider(
'collaborationThreadComments',
async (params, done) => {
// params = { threadId }
const comments = await fetchCommentsFromYourBackend(params.threadId);
done({ success: true, data: comments });
},
);
To make the builder re-fetch threads — for example after your backend receives new comments from another user — call:
unlayer.reloadProvider('collaborationThreads');
Callbacks
Register each callback with unlayer.registerCallback(name, handler). The builder calls them when the user makes a change; your handler persists it and calls done with the saved entity. On failure, call done({ success: false, error }) and the builder will discard the change.
| Callback | Payload | Expected result |
|---|---|---|
collaboration:thread:added | { thread } — includes designId, itemId, type, text, user | { success: true, thread } — with server-assigned id, firstComment, timestamps |
collaboration:thread:modified | { threadId, data } — e.g. { status: 'resolved' } | { success: true, thread } |
collaboration:thread:removed | { threadId } | { success: true } |
collaboration:comment:added | { comment } — includes threadId, text, user | { success: true, comment } — with server-assigned id and timestamps |
collaboration:comment:modified | { commentId, data, threadId } | { success: true, comment } |
collaboration:comment:removed | { commentId, threadId } | { success: true } |
Example: creating a thread
unlayer.registerCallback(
'collaboration:thread:added',
async ({ thread }, done) => {
if (!thread?.text || !thread.user?.id) {
done({ success: false, error: new Error('Invalid thread') });
return;
}
// Persist the thread and its first comment, assign IDs and timestamps
const savedThread = await saveThreadToYourBackend(thread);
done({ success: true, thread: savedThread });
},
);
When creating a thread, your backend should also create its first comment from the thread's text and user, and return the thread with firstComment, commentCount, status: 'open', and createdAt / updatedAt set.
Data model
Thread
| Field | Type | Description |
|---|---|---|
id | string | number | Unique thread ID (assigned by your backend). |
projectId | string | number | The project the design belongs to. |
designId | string | The design the thread is on. |
itemId | string | The ID of the design component the thread is anchored to. |
user | User | The user who started the thread. |
type | 'feedback' | 'idea' | 'question' | 'urgent' | Thread category. |
status | 'open' | 'resolved' | Thread state. |
commentCount | number | Total comments, including the first. |
firstComment | Comment | The comment that started the thread. |
createdAt / updatedAt | string | ISO 8601 timestamps. |
Comment
| Field | Type | Description |
|---|---|---|
id | string | number | Unique comment ID (assigned by your backend). |
threadId | Thread id | The thread this comment belongs to. |
user | User | The comment author. |
text | string | The comment body (plain text). |
createdAt / updatedAt | string | ISO 8601 timestamps. |
User
| Field | Type | Description |
|---|---|---|
id | string | Stable unique user ID. |
name | string | Display name. |
avatar | string | Image URL; initials are shown when empty. |
Tips
- Sort threads by
updatedAtdescending and comments bycreatedAtso the panel shows the latest activity first. - Bump the thread's
updatedAtwhen a reply is added so it surfaces at the top of the list. - When the last comment of a thread is removed, remove the thread as well.
- Enforce ownership server-side: only allow users to modify or remove their own threads and comments.