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.
What’s needed from your application
Section titled “What’s needed from your application”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.1Content-Type: application/vnd.openxmlformats-officedocument.wordprocessingml.documentX-FastDoc-Name: %D0%94%D0%BE%D0%B3%D0%BE%D0%B2%D0%BE%D1%80.docxThe 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.
Three steps
Section titled “Three steps”Start it
Section titled “Start it”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.
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.
From the archive without Docker
Section titled “From the archive without Docker”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.
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 editor’s title bar inside your page
Section titled “The editor’s title bar inside your page”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.
Operational flags
Section titled “Operational flags”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.
Proxy it through your own server
Section titled “Proxy it through your own server”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.
Give the link
Section titled “Give the link”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.
What happens under the hood
Section titled “What happens under the hood”- 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.
What a user without sign-in sees
Section titled “What a user without sign-in sees”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.
Two people in one document
Section titled “Two people in one document”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.
Who’s nearby
Section titled “Who’s nearby”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. |
The window’s top bar
Section titled “The window’s top bar”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.
Verification
Section titled “Verification”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.
- What embedding actually gives you and where the boundaries lie — Responsibility and security.
- Other ways to deploy SumDoc — Platforms and deployment.
- The request format for the core — Request and response objects.