4.9 KiB
AGENTS.md
Core-side document invariants: branches, files, hooks, search, tags. Cross-service storage rules (Hocuspocus, Yjs) live in server/AGENTS.md; Go standards in server/core/AGENTS.md.
Branches
- The main branch is identified by its
defaultflag, not its name: the tree join keys on it, andDocument.AllowsBranchRenamerefuses to rename it.NewDocumentandDuplicatesetDefault: true; forks must not. - Maintainers accumulate. The
maintainersfield on a branch update is who edited in that persist;UpsertDocumentMaintainersonly adds. There is no removal path. - Duplication copies, never shares:
Document.Duplicateregenerates every block uid, rewrites image/filesrcpaths to the new document (the file id is read off the src), and returns old→new maps for files (copied server-side) and block uids (hooks re-anchored; unmapped ones dropped).
History entries
document_branch_history_entries are restorable snapshots: name, icon,
content, hook definitions (type, block, settings; never watcher state)
and the author. Every write goes through
RecordDocumentBranchHistoryEntry, which locks the branch row, then
reads it and its hooks, in the transaction that wrote the change.
- Edits in one 30-minute bucket (by
created_at) update the newest entry and itsupdated_at. An entry equal to the newest is skipped. A null author (system write) never replaces a set one. - Hook create, update and delete flush the branch, then record an entry in their transaction. Reset does not.
- Create, duplicate, fork and merge write a boundary entry that never
aggregates, written after
CopyHooksso it lists the copies. - Entries pin the files they reference, so the
DB_*HISTORY*limits decide how long a removed image survives. Retention keeps each branch's newest entry.
Files and hooks
Files live at organizations/{org}/documents/{doc}/files/{fileId}. The id
is a nanoid the editor mints per upload, not the block uid, and an id that
already has a row is refused with 409, so a history entry keeps its object
when a block's file is replaced. GET /api/documents/{id}/files/{fileId}-{fileName} cuts the 21-char nanoid off
the front and ignores the name; Content-Disposition is inline only for
file.Viewable. The key is an S3 object key or a path under the storage
directory (internal/storage/{s3,fs}). Reclamation is by sweep, never by a
request handler:
- The row outlives its owner.
document_filesanddocument_hooksreference documents, branches and organizations withON DELETE SET NULL;document_files.storage_keystays reachable once FKs are gone. file/managerticks every 5 minutes: trims expired history, then reclaims files. A file is referenced if its id appears in any branch content, retained history entry or comment (CheckDocumentFileReferenced, in SQL). Unreferenced files are stampedunreferenced_atand deleted a day later; NULL-FK rows skip the wait; files younger than a day are never touched.hook/managerdoes the same for hooks, callingHook.Deletebefore dropping a row with a NULL FK. A merge detaches the target's hooks (DetachDocumentHooksByBranchID) rather than soft-deleting them.- Organization deletion is announced: auth-realtime's
beforeDeleteOrganizationcallsPOST /api/x/organizations/{id}/teardownwhile rows still exist and throws if core fails. - Create writes the row before the object; delete removes the object before the row.
- A null hook state means not set up. Copied hooks start null, never with the source's state; the operation sets them up after commit and the sweep retries what fails. Hook writes and the sweep share the manager's per-hook lock.
Search
- Jobs apply in order per document; a failed diff holds back every later one for that document, and an org removal holds everything after it.
- Entries are branch-scoped: id
<branchId>-<blockUid>(<branchId>-docnamefor the name), carryingdocumentId,branchId,branchName,branchDefault. Every branch change queues its diff throughsearch.Jobsin the same transaction (persist, fork, rename, merge); branch deletion queues aRemovedBranchesremoval, document deletion clears bydocumentIdusing the subtree idsagent.DeleteDocumentreturns. Index settings are sent only when they differ from the index; every write task is awaited.
Tags
Tags are per branch, visibility per user: tags (unique name per org,
sort_index for order), document_branch_tags (cascading from both),
user_tag_settings (tags a user hides). The sidebar tree lists a document
under a tag by its default branch; the header reads the open branch's tags
from GET /documents/{documentId}/branches/{branchId}/tags. A fork copies
tags, a merge makes the target carry exactly the source's, a duplicate
copies the default branch's.