Document editor architecture
The newsence document editor uses Tiptap 3 in the browser and stores validated Tiptap JSON in PostgreSQL JSONB. Markdown is an interchange format for AI context and edit tools; it is not the document database format.
Data flow
Section titled “Data flow”flowchart LR User["User edit"] --> Editor["Tiptap Editor"] Editor --> Draft["localStorage draft"] Editor --> Save["saveDocument server function"] Save --> Domain["document domain service"] Domain --> Current["user_documents.content JSONB"] Domain --> History["document_versions.content JSONB"] Domain --> Query["React Query cache"] Query --> Editor AI["Chat Worker edit-document tool"] --> Domain
web-tanstack/src/lib/editor/document-schema.ts owns the DOM-free schema shared by browser and Worker code. MARKDOWN_BRIDGE_EXTENSIONS must contain only extensions that can run without a browser. Browser-only behavior, such as pasted image uploads and placeholders, is added by the editor controller.
Browser editor
Section titled “Browser editor”web-tanstack/src/components/editor/useDocumentEditorController.ts is the orchestration boundary for:
- Tiptap editor creation and content validation;
- dirty state and save status;
- local draft persistence and recovery;
- throttled server autosave;
- optimistic cache updates;
- version conflict handling;
- version restore and deletion.
The controller uses Tiptap’s enableContentCheck. Invalid legacy JSON is kept intact, the editor becomes read-only, and automatic writes are disabled. Do not normalize invalid content during the read path: doing so could silently strip nodes before the user can recover them.
Local draft recovery
Section titled “Local draft recovery”Local drafts are stored under newsence:document-draft:{documentId} in localStorage.
- Serialization runs after a 400 ms trailing debounce.
- Pending draft and server-save work is flushed on
visibilitychangeandpagehide. - A draft is recovered only when its recorded document version matches the server version.
- Successful persistence of the current edit revision clears the draft.
Local recovery is best effort. The server remains authoritative after a successful save.
Autosave and concurrency
Section titled “Autosave and concurrency”Server autosave is throttled to a five-second trailing interval. Every write carries expectedVersion; the domain service performs an atomic UPDATE ... WHERE version = expectedVersion and increments the version only when title or content changed.
If the expected version is stale, automatic writes pause and the local draft is retained. A manual save or restore is the explicit local-wins action. React Query may replace editor content with a newer remote version only when the editor has no dirty local revision.
Version history
Section titled “Version history”user_documents stores the current version. document_versions stores snapshots with one of three sources:
auto-save;ai-edit;restore.
Ordinary autosave snapshots are throttled to one per ten minutes. AI edits and explicit restores create snapshots immediately. Restoring a snapshot writes it as a new current version instead of rewinding or deleting history.
Markdown bridge and AI edits
Section titled “Markdown bridge and AI edits”web-tanstack/src/server/domain/documents.ts constructs one MarkdownManager from the shared extensions.
contentToMarkdown()serializes validated Tiptap JSON for AI context and display-oriented consumers.markdownToTiptapJson()parses AI-created or AI-edited Markdown back into validated document JSON.- Database writes always receive Tiptap JSON.
AI edits operate on a Markdown projection, require each replacement target to occur exactly once, then parse the result back to Tiptap JSON before saving through the same optimistic-concurrency path as browser edits.
Images
Section titled “Images”Pasted and dropped images upload through the app-owned media boundary. Documents may not be saved while an image node still points to a transient blob: or data: URL. The server rejects these values as a final guard.
Media deletion is also ownership-aware: an asset referenced by a document cannot be deleted until the document stops referencing it.
Static rendering and sharing
Section titled “Static rendering and sharing”Read-only document pages use @tiptap/static-renderer with the shared schema. This avoids mounting an editable browser instance for shared pages and preserves task-list rendering.
Sharing metadata (share_enabled, share_slug, published_at, and description) is separate from document content. Publishing requires the owner to have a username; disabling sharing leaves the document and its history intact.
Verification checklist
Section titled “Verification checklist”For editor changes, verify all of the following:
- create, edit, reload, and manually save a document;
- recover a same-version local draft;
- trigger a two-tab version conflict without losing either draft;
- apply an AI edit and confirm the version source;
- restore an older version and confirm history remains available;
- upload an image and reload the document;
- render the document through its public share page;
- run
pnpm check,pnpm run typecheck, andpnpm buildinweb-tanstack/.