Why Collaborative Writing Tools Need Datoms, Not Documents

Google Docs revolutionized collaborative writing. But it's fundamentally flawed.

Not because of features. Not because of UX. But because of architecture.

Google Docs stores documents. Collaborative writing needs datoms.

The Document Model Is Wrong for Collaboration

Think about how Google Docs works:

  1. You type in a browser
  2. Changes stream to Google's servers
  3. Server merges your edits with others' edits
  4. Server sends merged document back to everyone
  5. Your browser re-renders

Problems:

  • Central server required - no offline editing
  • Operational Transform complexity - fragile merge logic
  • No true version history - only snapshots
  • Can't query relationships - comments are UI, not data
  • Vendor lock-in - your writing lives on Google's servers

These aren't bugs. They're inevitable consequences of the document model.

Enter Ellipsus: A Better Vision

Ellipsus is a collaborative writing tool that gets the philosophy right:

  • "Your content is YOURS"
  • No generative AI (ever)
  • Multi-device accessibility
  • Draft versioning and merging
  • Comments tied to specific drafts
  • Built for creative writers, not corporate docs

The vision is perfect. But can the architecture deliver on the promises?

What Ellipsus Needs (Whether They Know It or Not)

1. Conflict-Free Collaborative Editing with Local History Preservation

The problem: Alice and Bob both edit paragraph 3 simultaneously.

Traditional approach (Operational Transform):

Alice: "The hero hesitated." → "The protagonist paused."
Bob:   "The hero hesitated." → "The hero froze."

OT Algorithm:
1. Transform Alice's op relative to Bob's op
2. Transform Bob's op relative to Alice's op
3. Apply both in canonical order
4. Hope nothing breaks
5. Alice's original edit is LOST in the merge

OT is notoriously complex. Google Wave famously failed partly due to OT bugs. Even Google Docs occasionally shows merge artifacts. Worse: your local edits can be overwritten by the merge. What you wrote is gone.

DaoDB approach: Git-like branching + CRDTs

The key insight: local history should never be lost. Every device maintains its own branch. CRDT merges create a new merged branch, but your original edits remain queryable forever.

;; Alice's local branch (her device)
[para-3 :text/content "The hero hesitated." t1 meta-alice-1]
[para-3 :text/content "The protagonist paused." t2 meta-alice-2]
[meta-alice-1 :meta/branch "alice-laptop"]
[meta-alice-2 :meta/branch "alice-laptop"]

;; Bob's local branch (his device)
[para-3 :text/content "The hero froze." t3 meta-bob-1]
[meta-bob-1 :meta/branch "bob-desktop"]

;; CRDT merge creates new branch (preserves parents)
[para-3 :text/content "The hero froze." t4 meta-merged-1]
[meta-merged-1 :meta/branch "merged"]
[meta-merged-1 :meta/parents [meta-alice-2 meta-bob-1]]
[para-3 :merge/parents "alice-laptop" t4]
[para-3 :merge/parents "bob-desktop" t4]
[para-3 :merge/strategy :last-write-wins t4]

;; Alice's local history NEVER deleted
;; She can always query: "What did I write before the merge?"

Three document views:

  • Working View - shows merged state (what everyone sees)
  • My History - shows your local edits before merge (never lost)
  • Diff View - shows what changed in merge
;; Query: What does Alice see in "Working View"?
(view-merged db para-3)
;; => "The hero froze." (Bob's edit won)

;; Query: What does Alice see in "My History"?
(view-local-history db para-3 "alice-laptop")
;; => ["The hero hesitated."
;;     "The protagonist paused."]

;; Query: Did merge change Alice's text?
(merge-diff db para-3)
;; => {:mine "The protagonist paused."
;;     :merged "The hero froze."
;;     :changed? true}

UI affordances for writers:

  • Button: "Show My Edits" - see your creative process
  • Button: "Revert to My Version" - undo merge, keep your text
  • Notification: "Your text was changed by collaborator"
  • Timeline: "How did this paragraph evolve?" (your branch only)

Benefits:

  • No central server needed
  • Offline editing just works
  • Deterministic merge (same result on all devices)
  • No OT complexity
  • Local edits never lost - preserved in your branch
  • Safe collaboration - merge doesn't destroy your work
  • Undo merges - revert to pre-merge state anytime

2. True Version History, Not Snapshots

Google Docs "version history" is just periodic snapshots. You can't answer:

  • "Who wrote this specific sentence?"
  • "What edits happened between 2pm and 5pm?"
  • "Show me the evolution of paragraph 7"
  • "When did Alice and Bob's edits diverge?"

With DaoDB, every edit is preserved:

;; Query all versions of a paragraph
[:find ?content ?time ?author
 :where
 [para-7 :text/content ?content ?tx]
 [?tx :db/txInstant ?time]
 [?tx :db/author ?author]]

;; Result: Complete provenance
"First draft" - 2024-11-01 10:23 - Alice
"Revised for clarity" - 2024-11-01 14:15 - Alice
"Added emotional depth" - 2024-11-02 09:30 - Bob
"Final version" - 2024-11-03 16:45 - Alice

Time-travel queries:

;; What did the document look like on Nov 2nd?
(as-of db #inst "2024-11-02")

;; What changed between Nov 1 and Nov 3?
(diff
  (as-of db #inst "2024-11-01")
  (as-of db #inst "2024-11-03"))

This is impossible with the document model. It's trivial with datoms.

3. Comments as First-Class Data

In Google Docs, comments are UI elements. They're not queryable. You can't ask:

  • "Show all unresolved comments by Bob"
  • "Which paragraphs have the most discussion?"
  • "What feedback did Alice give on action scenes?"

With datoms, comments are data:

;; Comments reference paragraphs via entity IDs
[comment-1 :comment/target para-42]
[comment-1 :comment/text "Love this tension!"]
[comment-1 :comment/author bob]
[comment-1 :comment/status :unresolved]

[comment-2 :comment/target para-42]
[comment-2 :comment/text "Could use more detail"]
[comment-2 :comment/author alice]
[comment-2 :comment/reply-to comment-1]

Now you can query:

;; All unresolved comments by Bob
[:find ?text ?target
 :where
 [?c :comment/author bob]
 [?c :comment/status :unresolved]
 [?c :comment/text ?text]
 [?c :comment/target ?target]]

;; Paragraphs with most discussion
[:find ?para (count ?comment)
 :where
 [?comment :comment/target ?para]
 :group-by ?para
 :order-by (desc (count ?comment))]

;; Threaded discussions
[:find ?thread
 :where
 [?root :comment/target para-42]
 (not [?root :comment/reply-to _])
 [?reply :comment/reply-to ?root]
 [(vector ?root ?reply) ?thread]]

Comments survive text edits:

;; Paragraph content changes
[para-42 :text/content "The hero hesitated..." t1]
[comment-1 :comment/target para-42 t2]  ;; Attached
[para-42 :text/content "The protagonist paused..." t3]
;; comment-1 still attached to para-42 (entity ID stable)

4. Draft Branching and Merging

Ellipsus advertises "draft merging." This is a version control problem.

Git works for code because code is line-oriented text. Prose isn't. Sentence breaks, paragraph flow, narrative structure - these don't map to line diffs.

With datoms, merging is semantic:

;; Create alternate draft
(branch! main-draft alternate-draft)

;; Alice works on main-draft
[para-1 :text/content "Opening scene" t1 main-draft]
[para-2 :text/content "Rising action" t2 main-draft]

;; Bob works on alternate-draft
[para-1 :text/content "Different opening" t1 alternate-draft]
[para-3 :text/content "New subplot" t3 alternate-draft]

;; Find differences
[:find ?para ?main-content ?alt-content
 :where
 [?para :draft/parent main-draft]
 [?para :text/content ?main-content]
 [?para-alt :draft/parent alternate-draft]
 [?para-alt :paragraph/position ?pos]
 [?para :paragraph/position ?pos]
 [?para-alt :text/content ?alt-content]
 [(not= ?main-content ?alt-content)]]

;; Merge: User chooses which version to keep
(merge-drafts! main-draft alternate-draft
  {:para-1 :keep-alternate  ;; Use Bob's opening
   :para-2 :keep-main        ;; Keep Alice's rising action
   :para-3 :keep-alternate}) ;; Add Bob's subplot

This is semantic version control, not text diffs.

5. Branching Architecture: The Context Field

The secret to never losing local history is in the datom structure itself: [e a v t m].

The c field (context) becomes the branch identifier:

;; Every datom knows which branch it belongs to
[para-42 :text/content "Draft 1" t1 meta-1]
[para-42 :text/content "Draft 2" t2 meta-2]
[meta-1 :meta/branch "alice-laptop-local"]
[meta-2 :meta/branch "alice-laptop-local"]

;; After CRDT merge
[para-42 :text/content "Merged" t3 meta-merged]
[meta-merged :meta/branch "merged"]
[meta-merged :meta/parents ["alice-laptop-local" "bob-desktop-local"]]

Workflow: Local editing never touches merged branch

;; 1. Alice writes offline
[para-1 :text/content "Chapter 1 draft" t1 meta-a1]
[para-1 :text/content "Chapter 1 revised" t2 meta-a2]
[meta-a1 :meta/branch "alice-laptop-local"]
[meta-a2 :meta/branch "alice-laptop-local"]

;; 2. Bob writes offline (different device)
[para-1 :text/content "Alternative opening" t3 meta-b1]
[meta-b1 :meta/branch "bob-desktop-local"]

;; 3. Devices sync: CRDT merge creates NEW branch
[para-1 :text/content "Alternative opening" t4 meta-m1]
[meta-m1 :meta/branch "merged"]
[para-1 :merge/parents "alice-laptop-local" t4]
[para-1 :merge/parents "bob-desktop-local" t4]
[para-1 :merge/winner "bob-desktop-local" t4]  ;; Bob's timestamp later

;; 4. Alice's local branch UNTOUCHED
;;    Query meta entities with :meta/branch "alice-laptop-local" still returns:
;;      meta-a1 (t1): "Chapter 1 draft"
;;      meta-a2 (t2): "Chapter 1 revised"

Experimental branches: safe creative exploration

;; Try a radical rewrite without risk
(branch! "alice-laptop-local" "alice-experimental")

;; Edit experimental branch
[para-42 :text/content "Radical rewrite" t5 meta-exp1]
[meta-exp1 :meta/branch "alice-experimental"]

;; Original still exists
[para-42 :text/content "Original" t2 meta-orig]
[meta-orig :meta/branch "alice-laptop-local"]

;; Later: merge experimental back (or discard it)
(merge-branches! "alice-experimental" "alice-laptop-local")

;; Result: new merged datom, both parents preserved
[para-42 :text/content "Final" t6 meta-final]
[meta-final :meta/branch "alice-laptop-local"]
[para-42 :merge/parents "alice-experimental" t6]
[meta-final :meta/parents [meta-exp1]]

Query by branch: time-travel within your creative process

;; Show only my edits (before any merges)
[:find ?content ?time
 :in $ ?branch-name
 :where
 [para-42 :text/content ?content ?tx ?meta]
 [?meta :meta/branch ?branch-name]
 [?tx :db/txInstant ?time]
 :order-by ?time]

;; Pass "alice-laptop-local" → your creative evolution
;; Pass "merged" → converged state with collaborators

This is Git for prose, but better: merges are automatic (CRDT), conflicts are impossible, and history is queryable.

6. Multi-Device Sync Without Servers

Ellipsus promises multi-device accessibility. But most tools require a central server.

DaoDB's distributed architecture:

  • Each device has complete local database
  • Sync peer-to-peer (or via relay server)
  • Offline-first: write on plane, sync when landed
  • CRDT merge ensures convergence
  • No central authority required
Phone: Edit chapter 3 (offline)
  [para-15 :text/content "New scene" t1 phone-ctx]

Laptop: Edit chapter 5 (offline)
  [para-23 :text/content "Climax" t2 laptop-ctx]

Sync when both online:
  Phone  ←→ CRDT Merge ←→ Laptop

Result: Both devices have both edits
  No conflicts, no server, no cloud required

Optional cloud backup:

Local DaoDB → Encrypted .db file → S3 backup
(But not required - fully local-first)

What This Enables That Doesn't Exist Today

1. Never Lose Your Creative Decisions

;; Your complete creative evolution (your branch only)
[:find ?content ?time
 :in $ ?branch-name
 :where
 [para-7 :text/content ?content ?tx ?meta]
 [?meta :meta/branch ?branch-name]
 [?tx :db/txInstant ?time]
 :order-by ?time]

;; Result: Every version you wrote
10:23 "The hero hesitated"
10:25 "The protagonist hesitated"  ;; Revision
10:27 "The protagonist paused"     ;; Refinement
11:15 "She paused at the threshold" ;; Major rewrite

;; Even if CRDT merge chose Bob's version
;; Your creative process is preserved forever

Writers can ask:

  • "What did I write before the merge overwrote my text?"
  • "Show me every version of this opening paragraph I tried"
  • "When did I decide to change from 'hero' to 'protagonist'?"
  • "Compare my original draft to the merged version"

2. Queryable Writing Analytics

;; Word count by author
[:find ?author (sum ?words)
 :where
 [?para :text/author ?author]
 [?para :text/word-count ?words]]

;; Most-edited paragraphs (revision heat map)
[:find ?para (count ?edit)
 :where
 [?para :text/content _ ?tx]
 :group-by ?para
 :order-by (desc (count ?edit))]

;; Writing velocity over time
[:find ?date (sum ?words-added)
 :where
 [?para :text/content ?content ?tx]
 [?tx :db/txInstant ?instant]
 [(extract-date ?instant) ?date]
 [(word-count ?content) ?words-added]
 :group-by ?date]

;; Collaboration graph
[:find ?author-a ?author-b (count ?interaction)
 :where
 [?para :text/author ?author-a]
 [?comment :comment/target ?para]
 [?comment :comment/author ?author-b]
 [(not= ?author-a ?author-b)]
 :group-by [?author-a ?author-b]]

2. Content Provenance

;; Who wrote this sentence?
[:find ?author ?time
 :where
 [?para :text/content "It was a dark and stormy night." ?tx]
 [?tx :db/author ?author]
 [?tx :db/txInstant ?time]]

;; When was this paragraph added?
[:find (min ?time)
 :where
 [para-42 :text/content _ ?tx]
 [?tx :db/txInstant ?time]]

;; Show me Alice's contributions
[:find ?content
 :where
 [?para :text/author alice]
 [?para :text/content ?content]]

3. Structural Queries

;; Find all dialogue by character
[:find ?text
 :where
 [?para :paragraph/type :dialogue]
 [?para :dialogue/character "Sherlock Holmes"]
 [?para :text/content ?text]]

;; Chapters with unresolved plot threads
[:find ?chapter
 :where
 [?chapter :chapter/plot-threads ?thread]
 [?thread :plot/status :unresolved]]

;; Scenes with specific settings
[:find ?scene ?text
 :where
 [?scene :scene/location "London"]
 [?scene :scene/time-period "Victorian Era"]
 [?scene :text/content ?text]]

These queries are impossible with traditional document editors. With datoms, they're trivial.

The Data Ownership Revolution

Ellipsus promises: "Your content is YOURS."

But what does that mean in practice?

With Google Docs:

  • Content stored on Google's servers
  • Export as .docx (loses comments, version history)
  • Can't query or script
  • Vendor lock-in

With DaoDB:

~/Documents/Ellipsus/my-novel.db

SQLite file on YOUR disk
- Query with SQL
- Script with nbb-logseq
- Back up to Git
- Export as EDN (full fidelity)
- Switch tools (data portable)

You literally own the database.

This is the difference between claiming data ownership and delivering it.

Why This Matters for Creative Writers

Creative writing has unique needs:

Long-Term Projects

Novels take years. You need confidence your tool will exist, won't change pricing, won't shut down.

Solution: Local-first storage. Even if Ellipsus (or any tool) shuts down, you have the database.

Non-Linear Structure

Writers jump between chapters, create alternate scenes, experiment with structure.

Solution: Branch drafts, query relationships, merge semantically.

Collaboration Patterns

Co-authors, editors, beta readers - different access levels, different workflows.

Solution: Comments as data, queryable feedback, granular provenance.

Research and Worldbuilding

Writers need to track characters, locations, timelines, plot threads.

Solution: Custom attributes via datoms:

[character-1 :character/name "Sherlock Holmes"]
[character-1 :character/first-appearance para-42]
[character-1 :character/relationships [["Watson" :friend]]]

[location-1 :location/name "221B Baker Street"]
[location-1 :location/scenes [scene-1 scene-5 scene-12]]

[timeline-1 :timeline/event "Watson meets Holmes"]
[timeline-1 :timeline/date "January 1881"]
[timeline-1 :timeline/chapter chapter-1]

Now you can query:

;; Where does Sherlock appear?
[:find ?scene
 :where
 [?char :character/name "Sherlock Holmes"]
 [?scene :scene/characters ?char]]

;; Timeline of events
[:find ?event ?date
 :where
 [?t :timeline/event ?event]
 [?t :timeline/date ?date]
 :order-by ?date]

;; Plot holes: characters mentioned but never introduced
[:find ?name
 :where
 [?para :text/content ?content]
 [(extract-names ?content) ?name]
 (not [?char :character/name ?name])]

The Architecture: How It Would Work

Client Application

Ellipsus App (ClojureScript + React)
  ↓
DaoDB Client (DataScript in-memory + SQLite persistence)
  ↓
Local Filesystem: ~/Documents/Ellipsus/*.db

Data Model

;; Schema (example)
{:paragraph/content {:type :string}
 :paragraph/position {:type :number}
 :paragraph/author {:type :ref}
 :paragraph/chapter {:type :ref}

 :comment/target {:type :ref}
 :comment/text {:type :string}
 :comment/author {:type :ref}
 :comment/status {:type :enum [:unresolved :resolved]}
 :comment/reply-to {:type :ref}

 :chapter/title {:type :string}
 :chapter/position {:type :number}
 :chapter/draft {:type :ref}

 :draft/name {:type :string}
 :draft/created {:type :instant}
 :draft/parent {:type :ref}}

Sync Protocol

Device A: Makes edits → Datoms with vector clock
  ↓
Sync Layer: CRDT merge (deterministic convergence)
  ↓
Device B: Receives datoms → Merges into local DB
  ↓
Both devices: Identical state (eventually consistent)

Optional Features

  • Encrypted sync relay: For devices behind NAT
  • Cloud backup service: Encrypted .db files in S3
  • Public sharing: Export to web (static HTML from datoms)
  • Plugin system: Users write Datalog queries for custom features

What Ellipsus Could Differentiate On

With DaoDB as the foundation, Ellipsus could market as:

  • "The only writing tool where you own the database"
  • "Works offline, syncs everywhere, zero lock-in"
  • "Query your writing like a database (because it is)"
  • "True version control for prose, not code"
  • "Semantic merge: no more lost edits"
  • "Built on open formats: SQLite + EDN"

These aren't marketing fluff. They're architectural guarantees.

The Broader Lesson

This isn't just about Ellipsus. Every collaborative tool has this problem:

  • Notion: Documents locked in Notion's servers
  • Figma: Designs locked in Figma's cloud
  • Coda: Data locked in proprietary format
  • Airtable: Databases locked in Airtable's infrastructure

The pattern:

  1. Great UX
  2. Collaborative features
  3. Your data trapped
  4. Eventual enshittification (price hikes, forced upgrades, feature removal)

The alternative:

  1. Local-first storage (you own the .db file)
  2. CRDT sync (no central server required)
  3. Datoms as universal format (portable, queryable)
  4. Apps compete on interpretation, not data captivity

This is what Datom.world enables.

Conclusion: Storage Format Is Destiny

Google Docs stores documents. So it has:

  • Central server (no offline)
  • Snapshots (no true history)
  • UI-only comments (not queryable)
  • Vendor lock-in (data extraction lossy)

If Ellipsus stores datoms with branching, it gets:

  • Local-first (offline works)
  • Full provenance (every edit tracked)
  • Queryable everything (comments, versions, authors)
  • Data ownership (literal .db file on disk)
  • Local history never lost (your branch preserved forever)
  • Safe collaboration (merges create new branches, don't overwrite)
  • Git for prose (branch, merge, revert - but automatic)

Storage format is destiny.

Documents lose your edits when collaborators merge. Datoms preserve them in branches.

You can't build true collaboration on top of documents. You need streams of immutable facts with branch-aware storage.

That's what datoms with the c (context) field enable.

And that's why the future of collaborative writing - and every other collaborative tool - depends on them.

Learn More


Note: This is not a critique of Ellipsus. It's recognition that every collaborative writing tool faces these challenges. Ellipsus's philosophy (data ownership, no AI) aligns perfectly with what DaoDB enables. The question isn't whether they should use datoms, but when the entire industry realizes documents are the wrong abstraction.