# Restoring note content from a backup A note's live collaborative document is `note_states` (one row per note, the current Y.Doc, written only by the collaboration server), with periodic snapshots kept in `note_state_backups` (bucketed and pruned automatically). There is no restore CLI. Restoring a note's content — from a `note_state_backups` row, or from a `pg_dump`/`pg_restore` of the whole database after data loss — means writing a Y.Doc snapshot back into `note_states`, and that's **not** a plain `UPDATE` for two reasons: 1. If the `app` process is running, the collaboration server may already hold that note's document in memory. Overwriting `note_states` underneath it changes nothing observable — the in-memory copy stays authoritative until the server evicts it, and the next periodic store just overwrites your restore with the (stale/corrupt) in-memory content again. 2. `notes.links_clock` — the counter that gates `POST /api/notes/:id/links` writes (see [API contract summary](./api.md)) — only ever moves forward, compared against the *live* document's clock. A restored document's clock can be lower than the value already sitting in `links_clock` (the wikilink index and the Y.Doc are committed independently, on different schedules). Note loading already guards against this automatically — `onLoadDocument` clamps `links_clock` down to `LEAST(links_clock, )` every time a note is loaded, so a stale index can't actually lock future writes out on its own. Resetting `links_clock` to `0` as part of this procedure is belt-and-braces on top of that automatic clamp: it guarantees the gate is wide open regardless of what clock the restored document happens to carry, without you having to go work that clock out by hand. Notes in the trash are ordinary rows of `notes` (with `deleted_at` set), so a database backup includes them, and restoring the whole database brings the trash back as it was at the time of the backup. A note in it whose retention period has run out by then is permanently deleted again by the clean-up passes that start 5 minutes after the server starts (at most 2,000 notes per hourly pass); to keep such notes, turn automatic deletion off before starting the server (`UPDATE site_settings SET trash_retention_days = NULL`), have them restored from the **Trash** tab — by their owner, or for a group note by a member whose role can edit and delete notes (site admins see only their own there) — then set the retention period again in **Site admin → Storage plans → Trash**. Attachments of notes permanently deleted since the backup come back only if the uploads directory is restored from the same point in time. Procedure, to evict a note from a running server and restore it without restarting the whole instance: 1. Confirm nobody is currently connected to the note (see the warning below — this is not optional). 2. Force-close the note's live connections: `hocuspocus.closeConnections(noteId)`. 3. Flush any in-flight debounced store for it: `hocuspocus.flushPendingStores()`. 4. Call `hocuspocus.unloadDocument(doc)` and poll `hocuspocus.documents.has(noteId)` until it's `false` — an in-flight store makes `unloadDocument` a silent no-op, so this has to be retried on a short interval, not called once and trusted. 5. With the document confirmed evicted, do three things **in one database transaction**: 1. Write the restored content into `note_states`, raising its `version` by one in the same statement (`SET ydoc = …, version = version + 1`). 2. Clear the note's search index, the state row first: `DELETE FROM note_search_state WHERE note_id = :noteId; DELETE FROM note_search_sections WHERE note_id = :noteId;`. Keep that order — the server's own indexing locks the state row before it touches the section rows, and deleting in the opposite order can deadlock with an indexing run on the same note (PostgreSQL then aborts one of the two transactions). 3. Run `UPDATE notes SET links_clock = 0 WHERE id = :noteId` — this resets the gate so the next client's wikilink submission is accepted no matter what clock the restored document happens to carry. The note's search index is rebuilt the next time someone edits the note, or when the server next starts; until then its body text can't be found by search, while its title still can. **If you skip both the `version` bump and the two `DELETE`s, the text from before the restore stays searchable, and search excerpts keep showing it, until the note is next edited.** 6. Only now lift whatever kept clients out in step 1 and allow reconnects. **This procedure cannot be run against a note with online users.** `closeConnections` force-closes the current WebSocket, but a connected browser's collaboration provider reconnects automatically — the reconnect races step 4's polling loop, the document gets reloaded from the pre-restore database before it ever fully unloads, and the loop either spins forever or converges on the wrong content. There is no connection gate for this the way there is for note deletion (which sets an internal `deleting` flag *before* closing connections so reconnect attempts are rejected — see `beforeNoteDeleted` in `apps/server/src/collab/hooks-impl.ts`); a restore has no equivalent, so the outside world genuinely cannot be reconnecting to this note while you run it. In practice this means either taking the whole instance offline (stop the `app` container — with no process running there's no in-memory state to fight, so steps 2–4 are unnecessary and you can run step 5's transaction — restored content with the `version` bump, the two search-index `DELETE`s, and the `links_clock` reset — directly via SQL) or, if you need to avoid a full outage, first confirming and enforcing that nobody is connected to that specific note. There's no packaged tool for steps 2–5 today — running them live means attaching a one-off script to the running server process (the `hocuspocus` instance exposed by `CollabServer`, see `apps/server/src/collab/server.ts`). The eviction (steps 2–4) and `links_clock` reset (step 5) portion of this sequence — the mechanics, not the online-user failure mode above, which isn't something a test can safely reproduce — is exercised in `apps/server/test/collab-links.test.ts:375` (its client disconnects deliberately, sidestepping the auto-reconnect race described above, rather than exercising it).