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:
- You type in a browser
- Changes stream to Google's servers
- Server merges your edits with others' edits
- Server sends merged document back to everyone
- 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 mergeOT 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 - AliceTime-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 subplotThis 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 collaboratorsThis 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 requiredOptional 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 foreverWriters 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/*.dbData 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:
- Great UX
- Collaborative features
- Your data trapped
- Eventual enshittification (price hikes, forced upgrades, feature removal)
The alternative:
- Local-first storage (you own the .db file)
- CRDT sync (no central server required)
- Datoms as universal format (portable, queryable)
- 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
- DaoDB: Distributed Datalog database for datom storage
- Logseq's Knowledge Graph Paradox: Why Markdown files aren't enough
- Datoms as Streams: Local-first sync architecture
- Datom.World and Wave Function Collapse: CRDT merge as quantum mechanics
- Structure vs Interpretation: Why storage format matters
- Datom.world: The datom-native ecosystem