typograph-editor-documentation
    Preparing search index...

    Working in one template together

    Everyone who has the same template open sees the others' pointers and selections, live (TYP-918). Each person's pointer is an arrow with their name in their color; what they have selected gets an outline in that color. Everyone in the template has a color of their own, the same on every screen: whoever has been in it longest keeps the color of their token, and someone whose color is taken gets the first free one (people[].color is that color). A person's color does not change when someone else arrives.

    It is off unless you pass collaboration. Without it the editor opens no connection and behaves exactly as before.

    What happens to edits depends on edit_together. With it, everyone who may edit edits at once and each edit reaches the others within a moment (see "Editing together" below); one window saves for everyone. Without it, nobody's edits travel, so one window edits at a time (see "One person edits at a time" below): the others comment or look until it is free, and saving stays your job, as it always was.

    const editor = new Typograph.Editor({
    container: '#app',
    host: { /* … */ },
    collaboration: {
    url: 'wss://collab.typograph.nl/collab',
    document_id: templateId,
    token: async (documentId) => {
    // Ask YOUR backend; it asks Typograph for a collab token with the user's own token:
    // POST /v1/file/templates/{documentId}/collaboration-token
    const response = await fetch(`/editor/${documentId}/collab-token`, { credentials: 'same-origin' });
    return (await response.json()).token;
    },
    },
    });

    Constructing the Editor once it is already running works too, for a page that lets the editor boot itself and wires it up on typograph_editor_ready: new Typograph.Editor({ collaboration }) takes the running editor over and joins the room.

    Field
    url the collaboration server's WebSocket endpoint. The editor adds its collaboration protocol to it (?protocol=2); see below
    document_id the template's id; everyone with the same id is in the same room
    token(document_id) returns a fresh collab token. Called on every connect and reconnect: a token lives two minutes, so do not cache one
    release() optional, () => Promise<boolean>: save, before this window gives the pen to the user's other window. Resolve false (the save failed) and this window keeps it
    catch_up() optional, () => Promise<void>: load the latest saved document, before this window starts editing after someone else did. Editing stays withheld until it settles
    edit_together optional, boolean: everyone who may edit edits at once and sees the others' edits within a moment (TYP-919). Without it one window holds the pen; see below
    version optional, string: the saved version of the template this window loaded, such as the storage's ETag. The room records the version it was started from; see room_version() and conflict()

    The token is issued by Typograph to a person who may view the template, for that template only. It carries the name, color and photo the others see, and whether the person may edit. Each window publishes those to the others itself; collab-service closes a connection whose window claims another user, name or role than its token says, so a modified page cannot appear as someone else or take the pen with a view-only token. Fetch it from your backend: the API call needs the user's access token, which should never reach the browser.

    • A pointer is drawn only to people looking at the same spread, and it sits on the same spot of the paper for everyone, whatever their zoom or scroll.
    • It disappears when the pointer leaves the canvas, and when the person closes the template or loses the connection.
    • A pointer glides to where it was reported rather than jumping. Its name tag folds away a few seconds after the person stops moving and comes back when they move; someone who has not moved for a minute is drawn faint.
    • A selection is outlined for everyone, whatever spread they selected it on, as long as the element is on the spread they are looking at.
    • With edit_together, what someone drags or turns moves on everyone's screen while they drag it, not only when they let go; what they resize shows as their outline growing, and the content follows when they let go (TYP-919). A selected group counts as one element; past 50 elements at once the others see the drag only where it lands.
    • With edit_together, a colour, tint or opacity changes on everyone's screen while someone holds the slider or the swatch, not only when they let go (TYP-1599). Only the values travel, at most 20 times a second, and the others draw the newest once a frame; the change itself reaches the document once, on release, as one undo step. Past 50 elements at once the others see it only when it lands. An editor without this sends no preview and ignores the others', so a room with older editors in it keeps working.

    The editor draws the pointers; who is in the template and whether the connection is up are yours to show, in your own header — avatars next to a Share button, a dot for the connection. Listen for:

    document.addEventListener('typograph_collaboration_changed', (e) => {
    const { status, people, seats } = e.detail;
    // status: 'connecting' | 'connected' | 'offline' | 'denied' | 'full'
    // seats: how many people a full room seats while status is 'full', null otherwise
    // people: [{ user_id, name, color, avatar_url, windows, editing, you }] — the user first, then everyone else, once per person
    // editing: { you, by, taken_over, catching_up, catch_up_failed, writer } — who holds the pen and who saves, see below
    });

    It fires when someone joins or leaves and when the connection changes, never for a pointer moving. Someone with the template open in two windows is one person (windows: 2), drawn once, at the window they moved in last. The user's own other windows are not drawn at all: a pointer or outline with your own name on it would read as something you did here. The user comes first, with you: true, once the token has said who they are: windows counts this window and their others, and editing is true when one of them holds the pen. Show their photo too, so they see themselves among the others. avatar_url is null when the person has no photo; show initials on their color then. denied means the server refused the token, or refused this editor as too old for the room (below); retrying will not help.

    collab-service renews every connection now and then, about every 15 minutes, so a user whose access to the template was taken away loses the room within that time. The editor reconnects at once with a fresh token, and status stays connected through it: you hear offline only when the renewal has not reconnected within five seconds. editing.writer follows the connection itself, so it may turn false and back for that moment; nothing is saved for everyone while the window has no room. people keeps the others through it too, and so do their pointers and the pen: the window shows them as it last saw them until the room speaks again, or for those five seconds.

    collab-service seats at most 8 people in a room (TYP-1605), counted by person whatever their role; a second window of someone seated takes no second seat. The next person's window reports full, with seats saying how many the room seats: it shows the template read-only, draws nobody, holds editing with the reason 'room_full' ('view_only' for someone who may only view), and asks for a seat again every 10 to 12 seconds. Once it has one it reports connected and gives editing back as soon as it has settled. Say on screen that the template is full and that it opens for editing once someone leaves; the editor's own mode switcher does that already.

    Editors that read the shared room differently never meet in it. Each editor declares its collaboration protocol on the WebSocket URL (?protocol=2 since 1.0.0-20261101.1), and collab-service refuses an editor older than the room's: an editor still open in a tab after a release that changed the room. That window reports denied and keeps working alone, like an editor without collaboration: it is never held, and editing.writer is false, so save there as you do while the connection is down. The others never see it. Reloading the page loads the current editor, which joins the room again. The browser console says why: [collaboration] refused: protocol-outdated: ….

    With edit_together: true nobody waits for a pen: every window that may edit edits, and each edit — moving, resizing, restyling, adding, deleting, grouping, reordering — reaches the others within a moment, element by element. Two people changing different properties of one element both keep their change; two people moving the same element end in the same place on every screen. A window that joins late, or loaded the template before the others edited, is brought up to date when it joins. Undo takes back your own step only, never what the others did since — not even a change of theirs to the same element after yours. A window whose token only lets the person view is held while it is in the room (hold_editing()): its edits would reach nobody and be overwritten by the next one that does.

    One window saves for everyone. Every window holds the same document, so one of them saving is enough, and two saving at different moments could write an older state over a newer one. detail.editing.writer is true in exactly one window that may edit — the same one in every window — and only that window's host should autosave. When it leaves, another window becomes the writer; save there if the document is changed. The writer's is_changed() turns true for the others' edits too, and typograph_document_changed fires for them with reason: 'remote'. A window that lost the room (status is not connected) is never the writer: it sees only itself, and the windows still in the room have chosen one among them. Whether such a window saves for itself meanwhile is the host's call — portal-ui does, so work is not lost while collab-service is unreachable, and the room's writer saves the merged document once it is back.

    While the room exists, it holds the document. The first window to arrive puts the template it loaded into the room; every window after is brought to what the room holds, and the writer saves that. A change made to the saved template outside the room meanwhile — through the API, by an agent, or by a window that does not edit together — is overwritten by the room's next save. The room ends when its last window leaves, and the next window to arrive starts it from the saved template again.

    The room does record which saved version it holds: the version its first window loaded, then each version the writer saved (collaboration.saved(version, document)), read with collaboration.room_version(). A host that saves for everyone can send it as If-Match, so the storage refuses a save over a template changed outside the room instead of overwriting it. On that refusal the host loads the newer template and hands it to collaboration.conflict(document, version), which merges it into the room (TYP-1140):

    • what the newer file changed since the room's last save reaches every window, the one saving too;
    • what the room changed and the file did not stays;
    • where both changed one element or one spread, the file wins for that element or spread: its change was saved, the room's not yet;
    • then the room holds the newer version, and once conflict() resolves the host saves again with If-Match on it.

    The room tells what the file changed by a hash it keeps of each element and spread as last saved, so any window that may edit can merge, not only the one that saved last. It compares an element by its properties, its group and its spread, not by its place among its siblings: a file that only reorders elements keeps the room's order. A window that is dragging gets the merge when it lets go, as any change of the others; when that is the window that merges, conflict() resolves only then, so its save carries the merge. Not merged yet: the swatch library, styles and fonts, and whether page 1 is on the left: a newer file's change to those is still overwritten by the room's save.

    A window on the pen in the same room — an embed without edit_together, or a page loaded before it was switched on — edits a document of its own that the windows editing together cannot hear. While one holds the pen, a window editing together waits: editing.you is false, by says who has the pen, and it is not the writer. It edits again when that pen is put down. The other way round is not answered: a window on the pen that finds a window editing together ahead of it waits too, and its take_over() resolves false after its time-out, because a window editing together never hands its document to one that cannot hear it. That window edits once the ones editing together have left, or after it is reloaded with edit_together.

    A spread's own bleed, colour, name and lock travel too, and so do pages: adding, duplicating, deleting, moving and keeping pages together reach the others, and so does a page's width and height (the parameter bar or the page size dialog, one step for everyone, facing pages included), and the page each person is looking at stays on their screen. Two people adding a page in the same place both keep theirs. Ruler guides travel with the page they are on: a guide dragged out of a ruler, moved, deleted or given a position in the parameter bar appears, moves or goes for everyone, and elements snap to it on every screen. Not yet: undo of a guide edit while editing together, which can do nothing for now.

    Swatches travel too. A colour picked for an element, a swatch added, changed or removed in the swatch panel, and a gradient reach the others, also while the swatch panel is still open: each change in it is sent as it is made. A window that joins keeps every swatch, the room's and those only its own template has. Undo works as for the rest of editing together, with one difference: it does not take a swatch away, nor put back an older version of one. The library stays as everyone has it, so a colour you picked and undid stays behind as an unused swatch. With an editor or a canvas from before this, the swatch panel offers no + and − while editing together (it says why).

    Not shared yet: the document's styles and fonts. So while editing together nothing that changes those is offered: POD settings cannot replace colours or fonts, and the Templates and Clips tabs are disabled, since their pages and clips bring swatches, styles and fonts of their own. They come back when the window stops editing together; a tab your page disabled itself stays disabled. While you drag or resize, the others' changes wait until you let go.

    Text travels as it is typed. Each text frame's story is shared keystroke by keystroke: what one person types appears in the other windows within a moment, two people typing in the same paragraph both keep what they typed, and making a word bold while someone types in it keeps both. A caret or selection moves with the text around it. Undo works as for the rest of editing together: it takes back your own steps, not what the others did.

    An editor from before live text in the same room — a page loaded before this release — still works with the others: it sees what was typed live when the typing run ends (the next click or command, or the window being hidden or closed), and what it types reaches the others when its own run ends. Between the two, a frame's text goes by the last typing run, as it did before live text: what a live window typed after the older editor last heard from it is lost when the older editor's run arrives. Reloading the older page ends it. A table is shared as a whole: two people editing in one table at once, the last one wins for the whole table.

    Without edit_together, two people editing one template would overwrite each other's work: whoever saves last wins. So one window holds the pen. The first window that may edit takes it; every other window is put in Commenting (or Viewing, when the host offers no commenting) and Editing is refused until the pen is free. When the holder leaves, the window that has been waiting longest picks it up, and its user is put back in the mode they were in. Every window reaches the same answer from the same room state, so nobody asks a server for the pen.

    Someone whose token says they may only view never takes the pen.

    The pen changes hands with the document saved. A window that picks up the pen after someone else held it shows the version from before their edits; editing that would overwrite them. So before it starts, it calls catch_up() and its host loads the latest saved document; editing.catching_up is true meanwhile, and the others do not count it as editing yet. When catch_up() rejects, editing.catch_up_failed turns true and editing stays withheld, because editing the version on screen would overwrite the saved one: tell the user and offer collaboration.retry_catch_up(). The editor's own mode label only says editing is paused, not that a load failed, so a host that passes catch_up() shows the failure and the retry itself (from typograph_collaboration_changed), as portal-ui does with its pen chip. Leaving the room (stop()) refreshes nothing on screen: a window that was out of date, or whose load failed or was still running, loads the latest version again before it edits after start(). A window that hands the pen on — see take_over() — calls release() first, so what it changed is saved before anyone else edits. Pass both, or two windows can still overwrite each other.

    detail.editing says who has it:

    Field
    you this window holds the pen
    by { user_id, name, color, avatar_url, same_person }: the window that holds it, or while it changes hands the one next in line; null when this window has it or nobody does. same_person is true when it is the user's own other window: offer take_over() rather than "wait for them"
    catching_up this window has the pen and is loading the latest version first (catch_up())
    writer this window saves for everyone: with edit_together one window that may edit, without it the window that holds the pen
    catch_up_failed that load failed: this window keeps the pen but may not edit until retry_catch_up() succeeds
    taken_over this window was editing and lost the pen to a window that started earlier. That happens only when two windows started at the same moment or met after a lost connection, and changes made here meanwhile may be overwritten: tell the user

    people[].editing is true for the person whose window holds the pen and has finished catching up. Show why the tools are gone — portal-ui puts a chip with the holder's face where its mode control sits, and over the user's own other window a card with an Edit here button — because a silently locked editor reads as a bug. The mode switch itself is the editor's: typograph_mode_changed fires as usual, and ui.get_editing_held() says whether Editing is withheld right now. A host that offers only editing and draws nothing itself still gets the editor's locked label ("View only", with the reason as its tooltip) while editing is held. When the hold lifts the window goes back to the mode it was in, unless the user picked another one meanwhile.

    Member
    editor.get_collaboration() the live connection, or null without collaboration
    collaboration.status() / collaboration.people() / collaboration.editing() the same as the event's detail, read when you need it
    collaboration.take_over() take the pen from the user's own other window: it saves (release()) and hands it over, and this window catches up before editing. Resolves true once the pen is this window's — it may still be catching up, which editing() reports — and false when the pen is someone else's or the other window did not let go within 10 seconds
    collaboration.room_version() with edit_together, the saved version of the template the room holds: the one it was started from, then each one its writer saved. null outside such a room, or when no host passed a version
    collaboration.saved(version, document) tell the editor this window's document was saved as version. A window editing together that may edit records it in the room, also when it stopped being the writer while the save was in flight; a window outside such a room keeps it for the room it starts next. document, the JSON that was saved (a string or parsed), is how the room later tells what a newer file changed; without it the room takes its own state for what was saved, which counts an edit made during the save as saved
    collaboration.conflict(document, version) the storage refused a save (412): hand over the newer template (a string or parsed; a document, or a bundle with it as template, as open_document() takes) and its version. It is merged into the room for everyone, and room_version() returns version. Returns a promise: true once the merge is on this window's canvas — a drag in progress holds it back until it ends — then save again. false when this window cannot merge — it does not edit together, may only view, or the document has no spread — and the newer file must not be overwritten
    collaboration.retry_catch_up() load the latest version again after editing.catch_up_failed; does nothing otherwise
    collaboration.stop() leave the room; the others' pointers and outlines disappear. start() afterwards joins it anew, behind whoever edits by then
    collaboration.latency_samples() milliseconds from someone's pointer moving to it being drawn here, newest last. Taken with the sender's clock, so only meaningful between two windows on one machine

    destroy() leaves the room as well. Leaving gives the pen back: stop() lifts the hold on this window, and the next window in line takes over.