Skip to content

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.

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.

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 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 visibilitychange and pagehide.
  • 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.

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.

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.

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.

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.

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.

For editor changes, verify all of the following:

  1. create, edit, reload, and manually save a document;
  2. recover a same-version local draft;
  3. trigger a two-tab version conflict without losing either draft;
  4. apply an AI edit and confirm the version source;
  5. restore an older version and confirm history remains available;
  6. upload an image and reload the document;
  7. render the document through its public share page;
  8. run pnpm check, pnpm run typecheck, and pnpm build in web-tanstack/.