Skip to content
SumOfficeSumOfficeSumOffice

Editor in the browser: embedding through iframe

How to embed the SumDoc editor into a web application: a ready-made image, two endpoints on your API, three steps to a working editor, co-editing, and what happens inside.

If your product lives in the browser, you can embed the editor with nothing for the user to install: the editor page opens inside your application, and the core computes the document on the server.

Two required endpoints on your API, and a third, optional one. If you already have accounts and files, both required ones already exist.

Endpoint Why What it must respond with
“who am I” — --auth-url Identify the user and the name that others in the document will see Accepts the browser’s cookies or Authorization header as is; responds with 2xx and JSON with an email or id field (the name comes from name, displayName or fullName), otherwise 401 or 403
“file by ID” — --attach-url Hand over the document and accept the edit GET …/{id}/content returns the DOCX only to someone who has permission; a 302 to a signed link is allowed. The same endpoint with /replace instead of /content accepts the edit: POST multipart/form-data, part file. If the GET response carried the X-Layers-Attachment-Version header, the editor adds ?expectedVersion=N when writing; on a version mismatch, respond 412 — someone else’s edit is not overwritten
“create file” — --create-url (optional) Open a copy for a second person POST with the raw DOCX bytes; the file name goes in a request header, percent-encoded (sample below); the response is JSON with the id of the new file

A file creation request looks like this — the header carries the name, the body carries the DOCX itself:

POST /api/files HTTP/1.1
Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.document
X-FastDoc-Name: %D0%94%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80.docx

The editor has no database, accounts or passwords of its own: you know the users and the permissions. The full contract with sample responses for every call is HOST-CONTRACT.md inside the image; there’s also a sample host you can run it against.

The image starts with one command. The port is published only on the local address — your proxy exposes it to the outside (step 2). Sandboxes live in the /data/cabins volume and survive an image update.

Terminal window
docker run -d --name sumdoc-webhost --restart unless-stopped \
-p 127.0.0.1:8090:8090 \
-v /srv/sumdoc-cabins:/data/cabins \
hissih/sumdoc-webhost:latest \
--base-path /sumdoc --max-cabins 12 --idle-min 20 \
--auth-url 'https://your-host/api/me' \
--attach-url 'https://your-host/api/files/{id}/content'

Check: curl http://127.0.0.1:8090/sumdoc/health responds with JSON containing "status": "ok" and "skewMin": 0. Zero means the shell and the core come from the same release; any other number means the image was built incorrectly.

Before starting, it’s useful to check the environment’s readiness: doctor (doctor.mjs in the delivery) says in plain language what’s missing — the core, the template, a busy port, or the reachability of your endpoints.

The sandbox manager is a Node.js program, and you can run it without a container: from the unpacked image or from the web host delivery. It then listens only on the local address — you can’t connect to it from outside, even from your own network — and prints its readiness as the first line.

Terminal window
FASTDOC_CLI_BIN="$CLI" \
WEBHOST_WS_DIR=/opt/fastdoc/webhost \
node webhost/cabin-manager.mjs \
--template /opt/fastdoc/blank.docx \
--docs-dir /opt/fastdoc/cabins \
--port 8090 \
--auth-url 'https://your-host/api/me' \
--attach-url 'https://your-host/api/files/{id}/content'

Two variables are required, and they behave differently. In the image both are already set; you only need to set them when starting manually.

FASTDOC_CLI_BIN is the path to the processor: it’s the same $CLI as on the Installation and verification page. Without it, the manager starts and responds, but no sandbox opens: there’s nothing to start the core with.

WEBHOST_WS_DIR is the folder that holds node_modules/ws: the manager loads the WebSocket library from there. It’s usually the same webhost folder it’s started from. Without this variable, the manager doesn’t start at all — it prints “WEBHOST_WS_DIR is not set” and exits immediately. If the folder is set but node_modules/ws isn’t in it, it crashes on the very first attempt to load the library.

The third variable isn’t required, but almost everyone needs it: WEBHOST_CHROME (the --chrome flag offers the same choice) decides what to do with the window’s top bar — it’s left over from the desktop app and is usually superfluous on someone else’s page.

Value What’s shown
full The whole bar: document name, save, print, autosave.
slim The document name is hidden — it’s already in the browser tab — the buttons stay.
none No bar at all.

The default value is none, so set the title bar explicitly if you need it. The choice doesn’t affect the document’s geometry: the core computes the canvas, and it doesn’t change with the bar mode.

The manager’s other flags change not the look but the environment’s behavior under load. All of them have defaults, and you don’t have to change them — but it’s worth knowing them, because they affect both the user’s sign-in and their unsaved document.

Flag Default What it does
--warm 2 How many sandboxes to keep ready in advance. Sign-in stops waiting on the process start and the document opening. The pool refills in the background after every one is handed out.
--warm-max four times --warm, but no less than eight The warm-up ceiling, so the pool doesn’t grow without limit.
--idle-min 30 How many minutes of idling before a sandbox shuts down. Anything the user hasn’t saved must be saved on your side by then.
--max-cabins 0 — computed from the machine The limit on simultaneous sandboxes. At 0, the manager takes half the machine’s memory and twice the core count, whichever is smaller, but no fewer than four and no more than sixty-four.
--access-recheck-min 5 How often to recheck access permission with your API. Between checks, the sandbox lives on the previous answer: revoke access, and the person keeps editing the document until the next check.
--static-cache-mb 64 The ceiling on the manager’s in-memory static asset cache.
--port 8090 The manager’s port.
--template The empty DOCX that warmed-up sandboxes start from.
--entry none A fallback pass: the address opens with it without your “who am I”.
--docs-dir The folder where the manager keeps working copies of documents, per sandbox.
--create-url none Your “create file” endpoint; needed only when a second reader is given a copy of a document that’s in use.
--auth-url none Your “who am I” endpoint. As long as it’s set, sign-in only goes through your permission check.
--attach-url none Your “file by ID” endpoint. The sandbox downloads the attachment using the request’s own credentials: your host checks permissions, and if it doesn’t hand over the file, there’s no way in. The sandbox binds itself to the “identity and attachment” pair — you can’t slip in someone else’s document.
--base-path none The subpath, if the service doesn’t live at the domain root, for example /sumdoc. The proxy passes the path as is; the manager strips the prefix itself.
--bind 127.0.0.1 or WEBHOST_BIND The listening address. In the image it’s set to 0.0.0.0: the loopback address inside a container is unreachable from outside. When starting manually, keep the loopback — the proxy exposes the service to the outside.
--public-url http://127.0.0.1:<port> The service’s external address, as WOPI storages see it and as used to set cookie attributes.
--ui-locale not set The sandbox interface language: ru, en or auto. Without the flag, the original Russian.
--audit-log not written The audit log file, one JSON line per event: open, refusal, save, version conflict, access revocation. Without the flag, no log is kept: a log with people’s identities is never started silently.
--secure-view off Protected view for all sandboxes in the environment: the document can be read, but not printed or copied. The restriction lives in the sandbox server; the person can’t lift it.
--watermark none The watermark text on printouts and in the exported PDF; {кто} inserts the name of the signed-in user. There’s no watermark on screen.
--prestart-max 3 How many sandboxes with a document can be started in advance on a signal from the viewing page (POST /prestart?doc=…), so the editor is ready on click. Warm sandboxes don’t take slots away from people.
--prestart-ttl-s 60 How many seconds without a signal from the page before a warm sandbox shuts down.
--prestart-origins none The origins (CORS) allowed to send the prestart signal.
--window-min-pages 0 — off From how many pages the document scene is sent to the browser as a window rather than in full: on long documents this cuts traffic and time to first display several times over.
--window-min-media-bytes 0 — off The same by image weight: a document heavier than the threshold is handled as a window, and images are sent on demand.
--wopi-hosts none A comma-separated allowlist of WOPI storages. Without the flag, WOPI is off.
--wopi-refresh-min 25 How often to refresh the WOPI lock, in minutes.

The web host can also open files over the WOPI protocol — a second driver for the same door as “who am I” and “file by ID”. It’s enabled only by the --wopi-hosts flag with a list of allowed storages; without the list, any WOPISrc is rejected. The manager serves /hosting/discovery and /hosting/capabilities, calls CheckFileInfo, GetFile and PutFile, holds and refreshes the lock (Lock, RefreshLock, Unlock), signs requests with proof keys, and supports frame postMessage. The storage answers 409 to a second person on a locked file: there’s no co-editing over WOPI, it works only through the first driver. Verified against our own sample WOPI host; not certified against Nextcloud, SharePoint or ownCloud.

An ordinary reverse proxy with one required condition: the switch to WebSocket must go through. Without it, the editor can’t reach the core.

A ready-made nginx sample is in the delivery, in the examples folder. Three things matter: passing the protocol-switch headers, passing your authorization cookies, and a long enough read timeout.

An “Edit” item appears next to the file in your interface:

<iframe src="/fastdoc/?doc=document-id"
style="width:100%;height:100%;border:0"
allow="clipboard-read; clipboard-write"></iframe>

A user signed in to your application lands in the editor with their own document. Edits go back into your file through your own API.

  • Every user gets their own sandbox — a separate process with its own document. Someone else’s document is never visible and can’t be slipped in.
  • Permissions stay with you. Don’t hand over the file, and the editor won’t open. And not just at sign-in: every few minutes, the editor asks again whether the person is still allowed to have the document they have open. Revoke access, and the sandbox shuts down, and the person sees your own response.
  • Edits go out through your API with the same user’s credentials.
  • A write refusal is visible to the person. If you respond with a permanent refusal — the session expired, or there’s no permission left — a bar appears at the top of the page: edits aren’t being saved right now, but the work isn’t lost. When writing succeeds again, the bar disappears.
  • A sandbox shuts down after idling and starts again on the next sign-in. You can keep some sandboxes warmed up in advance: then opening a new document takes a fraction of a second instead of waiting for the process to start.

The environment’s state is visible at /health: the build version and the shell-to-core skew (skewMin), how many sandboxes are alive, and how many write refusals, version conflicts, co-editing sessions and copies there were in the last hour.

The editor has no accounts or passwords of its own: it takes identity from your application. So a request without your session gets refused, and the person sees a short explanation — “sign-in required, sign in and open the document again”. This isn’t an embedding bug, it’s normal behavior: if a signed-in user sees this, the cookies never reached the editor — check that it opens over https, on the same domain, and that the proxy passes the cookies.

A second person on a busy document is shown a choice of three buttons.

“Edit together” — one file for two. Edits travel between sessions, the core computes the transfer, and everyone ends up with the same text; the other person’s input is visible immediately. Exactly one person writes to your storage — the holder, whoever opened the document first. The co-editor’s edits get there with the holder’s save, and if the holder leaves, writing passes to the co-editor. Two people writing to one file would silently lose each other’s work, hence the rule.

“Make my own copy” — a separate file on your side, its own browser address, and a bar saying “this is your copy, the original file is being edited by another person right now”. This is what the third, optional endpoint — “create file” — is for. Don’t provide it, and there’s no such button; the other two work. The copy gets a meaningful name if your file endpoint sends the usual header with the name: then it’s called “Договор №42 (копия).docx” (“Contract No. 42 (copy).docx”) rather than after the creation date.

“View without changing” — the same document, without editing. As soon as the holder’s edit reaches your storage, the viewer is told in words right in the tab that their copy is out of date, and the page rereads the fresh bytes. When the holder leaves and the document is released, the viewer is told too.

A document counts as busy only while the holder’s page is alive, not while the network socket is intact: a hung tab must not lock the file for everyone else. Every 15 seconds the manager compares the session state fingerprints; a diverged session is reread from the holder’s state.

How many times per hour the document was co-edited and how many times a copy was taken is visible in the same /health.

The other person’s edits appear as they type, colored in their color, with the name your “who am I” returned above the caret. If they’re on another page, the header shows where they’re editing; a click takes you there. At the bottom right is a subtle badge: “Also in the document: Boris. Boris writes to the shared file; your edits will get there with his save”. Alone in the document — no badge.

Undo is your own. Undo removes your edit, even if the other person typed ten letters over it: their letters stay.

The manager reports who’s nearby at GET /presence (by the sandbox cookie):

{ "schema": "fastdoc.webhost-presence.v1",
"simultaneous_editing": true,
"single_writer_role": "правит",
"others": [ { "name": "Борис", "role": "правит", "present": true } ] }
Field What’s in it
simultaneous_editing true — typing is simultaneous.
single_writer_role The role whose holder writes to the shared file. правит (editing) — the holder; правит вместе (editing together) — edits the same document but doesn’t write to the host; смотрит (viewing) — an observer.
others[] Only the people in this same document: name — the name from your host, with no addresses or sandbox numbers; role; present — the person is on the page right now, rather than gone and left the sandbox behind.

In the desktop app, a window bar with the document name and buttons runs along the top. In the browser, the document name is already in the tab, so you’d get two headings stacked on top of each other. That’s why the bar’s look is configurable:

Value What it does Who it suits
none — the browser default No bar, the ribbon starts right at the top Everyone opening the editor in the browser
slim The bar is there, but without the document name Anyone who wants its buttons close at hand
full Same as the desktop app Anyone who needs the exact same look as before

Only visibility changes: the core computes the document’s geometry, and the page doesn’t shift. With the bar turned off, saving stays in the “File” tab.

The delivery has three checks, and all of them run with a single command.

What to check With what
The environment’s readiness before startup doctor.mjs
Whether the whole embedding is correct acceptance.mjs — prints an “expected / got” table
Behavior under load load-test.mjs — on your own live sessions

A zero in the acceptance check’s “mismatched” row means the embedding is done correctly.

Documentation assistant

Answers are assembled from the documentation and may be inaccurate — check the sources.