> ## Documentation Index
> Fetch the complete documentation index at: https://docs.doconda.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Export on your server

> Turn a co-editing room into a .docx, .xlsx or .pptx in Node, stay connected to keep its state, and turn Markdown into .docx, with no browser and no React.

**Server export** turns documents into files in Node, with no browser and no React. You import `@doconda/editor/server`
on your own server and call a function. It's included in the [editor licence](https://docs.doconda.com/editor/license.md).

This is useful for:

* Saving a [co-editing room](https://docs.doconda.com/editor/collaboration.md) from your backend, instead of electing a browser to save for everyone.
* Turning Markdown into a Word file without mounting a hidden editor.

It needs Node 22 or later, which has `WebSocket` built in.

## Your documents stay on your server

Conversion and export run inside your process. Document content never goes through Doconda's servers because of this
function:

* **The only call to Doconda is the licence permit.** It sends your API key and the mark of your copy, and nothing
  about the document: no bytes, no file name, no room contents.
* **Reading a room goes only through the transport you choose.** With `url` or `provider`, Doconda isn't involved. With
  `token`, changes pass through Doconda's relay, which forwards them and stores nothing, like any co-editing session.

## Convert a file

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { readFile, writeFile } from "node:fs/promises"
import { convert } from "@doconda/editor/server"

const { bytes, warnings } = await convert(await readFile("notes.md"), {
  name: "notes.md",
  to: "docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})
await writeFile("notes.docx", bytes)
```

`name` is only used for its extension, which says what the bytes are. `warnings` lists what the engine had to report
while opening or saving.

| From | To |
| - | - |
| `.md` | `docx`, `md`, `txt` |
| `.docx`, `.txt` | `docx`, `md`, `txt` |
| `.xlsx`, `.csv` | `xlsx`, `csv` |
| `.pptx` | `pptx` |

PDF and PNG are refused with `format_unsupported`: PNG needs a canvas, and a PDF would be paginated without real font
measurements. To get a PDF, use `ref.save("pdf")` in the browser.

## Export a room

`exportRoom` joins the room, waits for what the others have, returns the file and leaves:

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { exportRoom } from "@doconda/editor/server"

const { bytes } = await exportRoom({
  url: `wss://collab.acme.com/?room=doc-${id}`,
  file: await readOriginalFile(id),
  name: "contract.docx",
  to: "docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})
```

| Option | What it is |
| - | - |
| `url`, `token` or `provider` | Exactly one, as in [`collab`](https://docs.doconda.com/editor/collaboration). `token` can be a function. |
| `file` | The file the room opened. `null` only if the room opened a blank document. |
| `name` | The file's name. Its extension says the document's type. |
| `to` | The format you want back. |
| `apiKey` | Your [API key](https://docs.doconda.com/quickstart.md), not the `ek_…` licence key: this code runs on your server, so it can hold a secret. Never use it in a page. |
| `timeoutMs` | How long to wait for an answer from the room. 10 seconds by default. |

Behavior to be aware of:

* **Pass the file everyone opened.** A room doesn't hold the whole document: what nobody touched comes from `file`. A
  different file isn't detected, and gives a wrong result.
* **It only reads.** It writes nothing to the room, whichever file you pass, and doesn't show up as a participant.
* **It reads only what's in the room at that moment.** Doconda's relay stores nothing, so when the last person leaves,
  there's nothing to read and you get `collab_empty_room`. To keep what was there, [stay in the room](#stay-in-a-room). A
  relay of your own that keeps the state works too, if it answers the hello (message type `2`) with that state.
* **An empty room is an error**: you never get the old file or a blank document back.
* A provider you pass stays open; closing it is up to you.

### On Doconda's server

Ask for a token with `audience: "server"`, using your API key. A normal token is refused without a browser's `Origin`.

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { Doconda } from "@doconda/sdk"
import { exportRoom } from "@doconda/editor/server"

const doconda = new Doconda() // reads DOCONDA_API_KEY

const { bytes } = await exportRoom({
  token: async () => (await doconda.editor.collabToken(`doc-${id}`, { audience: "server" })).token,
  file: await readOriginalFile(id),
  name: "contract.docx",
  to: "docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})
```

In Python, `doconda.editor.collab_token(room, audience="server")`. Without the SDK, send
`POST /editor/collab/token` with `{ "room": "…", "audience": "server" }`. Keep this token on your server.

## Stay in a room

`joinRoom` keeps a connection open and holds the document in memory, so it still has the state after everybody leaves:

```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { joinRoom } from "@doconda/editor/server"

const room = await joinRoom({
  url: `wss://collab.acme.com/?room=doc-${id}`,
  file: await readOriginalFile(id),
  name: "contract.docx",
  apiKey: process.env.DOCONDA_API_KEY!,
})

// later, as often as you like, also when nobody is left:
const { bytes } = await room.save("docx")

await room.close()
```

| Member | What it does |
| - | - |
| `save(to?)` | Returns the file with everything received so far, in the document's own format by default. |
| `contentVersion()` | A number that goes up with each change received. If it hasn't moved, there's nothing new to save. |
| `peers()` | How many people are in the room now. When it reaches 0, it's a good moment to save. |
| `status()` | `"connecting"`, `"open"` or `"closed"`. |
| `close()` | Disconnects and frees the document. Nothing stays running. |

It takes the same options as `exportRoom`, without `to`. Behavior to be aware of:

* **Nobody sees it.** It sends no cursor or presence and isn't counted in the others' `peers`. It writes nothing to the
  room. It only sends the hello that asks the others for their state and, when it closes a connection it opened, the
  standard "this connection left" message for its own ID, which changes nothing for the others. On Doconda's server it does
  take one of the 50 places in the room.
* **Join before people can leave.** It only knows what it received while connected. Join when the room opens.
* **If the connection drops, it reconnects by itself** and asks for the state again, so it recovers what was edited
  meanwhile if someone who has it is still connected. Pass `token` as a function: a fixed token is refused after 10 minutes.
* **If the room closes the connection for good** (a code from `4000` to `4999`), it keeps what it has. `save()` still works
  and adds a warning, because later edits may be missing.
* `save()` with nothing received yet waits up to `timeoutMs` and then fails with `collab_empty_room`.
* If `joinRoom` can't connect within `timeoutMs`, or the room refuses it, it fails at once with `collab_unreachable` or
  `collab_closed`.
* **One session per file.** Once you store a new version and people will open that one, `close()` and join again with it
  as `file`: a room's changes only make sense on top of the file it opened.
* The document lives in your process's memory: if the process restarts, what wasn't saved is gone.

## Which one to use

| | `exportRoom` | `joinRoom` |
| - | - | - |
| Reads | Only what's in the room at that moment. | Everything since it joined. |
| With an empty room | Fails with `collab_empty_room`: there's nothing to return. | Still returns what it received. |
| Connection | Opens, reads and closes. | Stays open until `close()`. |
| Use it for | A save or a download while someone is editing. | Never losing the last edit before the room empties. |

`exportRoom` arriving after the last person left gets nothing, because the relay stores nothing. If you need what was
there when everyone left, use `joinRoom`.

## The licence on your server

1. The function asks Doconda for a permit with your API key (`POST /v1/editor/server/permit`). There's no domain check, because
   a server has no domain.
2. Doconda checks the key and that your organization has an active licence, and returns a **signed** permit valid for **1 hour**.
3. The function checks the signature and keeps the permit in memory, renewing it when less than 15 minutes remain.

A server permit can't open the editor in a browser and doesn't let anyone into a room. There's no development mode: on
`localhost` you also need a key.

**If Doconda's API is unreachable**, a process that already holds a valid permit keeps working until the permit expires,
so up to 1 hour. A process with no valid permit fails with `editor_license_unreachable`, and can try again later. If Doconda
refuses the key (cancelled, rotated), the function stops at the next check.

A session from `joinRoom` checks its permit at each `save()` and renews it there, so it can stay connected for longer
than a permit lasts. An idle session makes no requests. **If the licence is revoked mid-session**, the connection and the
document stay, and `save()` fails with `editor_unlicensed` from the next renewal, so within 1 hour; if the licence comes
back, `save()` works again with nothing lost.

## Errors

Every failure throws a `DocondaServerError` with a `code`:

| Code | Meaning | What to do |
| - | - | - |
| `editor_unlicensed` | No key, a key Doconda refuses, or a permit that isn't valid. | Check the key and that the licence is active. |
| `editor_license_unreachable` | Doconda couldn't be reached and there's no valid permit. | Try again later. |
| `format_unsupported` | That type of file, or that output, isn't available on a server. | See the table above. |
| `file_invalid` | The file couldn't be opened. | Check the file and its extension. |
| `file_required` | `exportRoom` got no `file`. | Pass the file, or `null` for a blank document. |
| `collab_empty_room` | Nothing received from the room within `timeoutMs`. | Keep your saved copy, or use `joinRoom` so it's there when you need it. |
| `collab_closed` | The room refused the connection; `closeCode` says why. | See the [close codes](https://docs.doconda.com/editor/collaboration.md#troubleshooting). |
| `collab_unreachable` | The connection never opened within `timeoutMs`. | Check the address and your network. |

## Related

> [card] [Real-time co-editing](https://docs.doconda.com/editor/collaboration.md)
>
> Rooms, tokens and running your own server.

> [card] [Licence and domains](https://docs.doconda.com/editor/license.md)
>
> Your key, and what happens when the subscription ends.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.