Guidance for coding agents working in this repo.
http://localhost:6747— Vite dev server (frontend). (cd browser && pnpm dev)http://localhost:9883— local AtomicServer. (cd server && cargo run)
The frontend auto-updates via HMR. If changes don't appear, reload the page. If you edit @tomic/lib or @tomic/react, those packages may need a rebuild first.
Use the ./planning folder to write plans and keep track of progress.
Use todo lists and checkboxes to track progress.
Make sure to update the planning as you find new insights and see outdated planning text.
Remove planning documents that are completed.
Use the Charlotte MCP server and navigate to http://localhost:6747/app/dev-drive to instantly create a fresh agent.
In E2E tests, most specs use test.beforeEach(before) from test-utils.ts, which calls devDrive(page) and gives every test a fresh agent + drive. For a second browser context signed in as the same user, use getDevDriveSecret(page) after before has run. Call devDrive(page) directly only when a spec does not use the shared before hook.
- Operate the app at
localhost:6747for quick iterations on react code. - Start every session by navigating to
http://localhost:6747/app/dev-driveto get a clean, authenticated state. - If the app shows
UnauthorizedorSomething went wrong, navigate to/app/dev-driveto fix it.
- Identify the bug, where it's coming from.
- Reproduce the bug in a test at the right abstraction level. E2E tests are the most expensive, so try to find a different level if possible.
- After reproduction in a failing test, fix the bug until the test and all other tests are green again
In dev mode, window.devtools exposes diagnostics for inspecting a resource across every persistence layer. Run devtools.help() for the list. Most useful:
| call | what it does |
|---|---|
devtools.inspect(subject?) |
JS store + WASM/OPFS + server HTTP GET, side-by-side. Defaults to the URL's ?subject= (or current drive). |
devtools.opfsList(prefix?) |
Subjects in the WASM DB (default prefix did:ad:) |
devtools.wsLog(n?) |
console.table of the last N commit log entries |
devtools.problems() |
Resources currently loading, errored, or new |
devtools.forcePut(subject) |
Re-serialize a JS-store resource into OPFS with round-trip verification |
Source: browser/data-browser/src/helpers/devtools.ts.
Atomic Server is a graph database with real-time sync, built on Loro CRDT for conflict-free collaborative editing.
docs(docs) — Public-facing Atomic Data spec and product documentation. Describes how the protocol works, very important.planning(planning/) — Internal design notes and larger technical direction. Readplanning/README.mdand the relevant plan before broad architectural work.atomic_lib(lib/) — Core library powering atomic-server + WASM / OPFS browser storage.atomic-server(server/) — Actix-web HTTP/WS server. Usesatomic_lib+ search (tantivy).@tomic/lib(browser/lib/) — TypeScript client library, powering the other JS projects@tomic/react(browser/react/) — React hooks.data-browser(browser/data-browser/) — The web app (React + TipTap + Loro), feels similar to notion. See the related AGENTS.mdflutter/— Cross-platform canvas app (Android/iOS/Web). Usesflutter_rust_bridgeto callatomic_lib. Seeflutter/README.mdandflutter/AGENTS.md.
- Resource = property-value pairs with a Subject URL, backed by a Loro CRDT document.
- Commit = a signed mutation containing
loroUpdate(base64 Loro binary). - Agent = Ed25519 keypair, identified by
did:ad:agent:{publicKey}. - Drive = top-level container resource.
Loro is the sole state management engine. The old set/remove/push commit fields are deprecated and rejected by the server.
resource.set(prop, value)→ writes to LoroDoc's"properties"map + sets_dirtyresource.save()→exportLoroDelta()→ base64 → commitloroUpdate→ sign → POST- Incoming WS commits:
execLoroUpdateCommit()imports Loro binary into resource's LoroDoc, materializes properties into propvals
- Commit arrives at
/commit apply_changes()importsloroUpdateinto resource's LoroDocimport_update_with_diff()computes add/remove atoms for search indexingloro_value_to_atomic_value_tagged()materializes Loro values to AtomicValuetypes, using thedatatypesmap- Loro snapshot stored alongside PropVals for future merges
The LoroDoc has two sibling root maps:
properties—property URL → value. Loro primitives stored directly (strings, numbers, booleans); arrays as nativeLoroLists; objects as JSON strings.datatypes— sparseproperty URL → tag, recording the datatype only where a bare primitive is ambiguous in a load-bearing way. Tags:atomicUrl,resourceArray,json,resource. Scalars and plain/cosmetic strings carry no entry. Written byset_property(Rust) andResource.writeDatatypeTagsat sign time (TS).
Materialization prefers the tag: loro_value_to_atomic_value_tagged() recovers
the exact Value variant from it. Untagged values fall back to the
loro_value_to_atomic_value() heuristic (URL-shaped strings → AtomicUrl,
{...} → NestedResource), kept for legacy / not-yet-tagged docs. Cosmetic
datatypes (markdown/slug/date/uri, timestamp) are deliberately not
tagged — they collapse to string/integer; the Property's datatype stays
authoritative. See planning/loro-source-of-truth.md.
When editing a resource, load the existing Loro snapshot first, then edit on top. Creating a fresh LoroDoc for each edit causes LWW conflicts. The CommitBuilder on the server converts set/remove to Loro at sign time via sign_at().
{
"https://atomicdata.dev/properties/subject": "did:ad:{genesis}",
"https://atomicdata.dev/properties/signer": "did:ad:agent:{publicKey}",
"https://atomicdata.dev/properties/loroUpdate": "base64...",
"https://atomicdata.dev/properties/signature": "base64...",
"https://atomicdata.dev/properties/createdAt": 1775504552928,
"https://atomicdata.dev/properties/previousCommit": "did:ad:commit:{sig}",
"https://atomicdata.dev/properties/isGenesis": true
}loroUpdateis a plain base64 string (not a{type, data}object)set,push,removeare rejected by the server- Signature: deterministic JSON-AD (sorted keys, minified, no
@id, no signature field) - Genesis commits:
subjectexcluded from signed bytes (derived from signature)
Subject is an enum: Internal (internal:/path), External (https://...), Did (did:ad:{genesis}).
Commit.subject and Commit.signer are Subject, not String.
Equality is by URL string only — drive_hint and subdomain don't affect identity (custom PartialEq/Hash).
| Message | Direction | Purpose |
|---|---|---|
AUTHENTICATE {json} |
C→S | Auth |
AUTHENTICATED |
S→C | Confirmed |
SUBSCRIBE {subject} |
C→S | Commit notifications |
COMMIT {json} |
S→C | Applied commit |
LORO_SYNC_SUBSCRIBE {json} |
C→S | Real-time Loro sync |
LORO_SYNC_UPDATE {json} |
Both | Loro binary (base64) |
LORO_EPHEMERAL_UPDATE {json} |
Both | Cursors/presence |
Pattern: Subscribe to broadcast BEFORE sending a message that expects a response.
Uses ed25519-dalek (pure Rust, WASM-compatible). Server keeps ring for TLS only.
pub struct Resource {
propvals: PropVals, // Read cache
subject: Subject,
commit: CommitBuilder, // Legacy server-side
loro: Option<AtomicLoroDoc>, // CRDT doc, lazy
}save()— server-side (CommitBuilder → Loro → apply locally)save_remote(store)— client-side (propvals → Loro → export → sign → HTTP POST)save_as_genesis(store)— DID resource, subject =did:ad:{signature}
TipTap + loro-prosemirror (LoroSyncPlugin, LoroUndoPlugin, LoroEphemeralCursorPlugin).
Real-time: useLoroSync hook → LORO_SYNC_UPDATE WebSocket.
Loro OpLog time-travel: doc.getAllChanges() → sort → doc.checkout(frontiers) per version. Instant, no network round-trips.
Devices sync via Iroh QUIC connections. The transport is in lib/src/sync/:
peer.rs— Iroh endpoint, Router (must stay alive for incoming connections), persistent NodeID (secret key stored in redb), known peers list.engine.rs— Transport-agnostic sync engine. Compares Loro version vectors, computes diffs, imports snapshots. Used by both WS and Iroh.protocol.rs— Binary frame encoding: AUTH, SYNC, SYNC_DIFF, SYNC_PUSH, SYNC_OK, GET, UPDATE.
- Both devices start Iroh (
peer::start()) → get persistent NodeID, connect to n0 relay - Device A shows QR code containing
did:ad:node:<nodeId> - Device B scans QR → calls
peer_sync(nodeId)→sync_drive_with_peer() - B→A: AUTH, SYNC (with B's version vectors)
- A→B: SYNC_DIFF (what to push/pull), SYNC_PUSH (A's data)
- B→A: SYNC_PUSH (B's data for A's pull list)
- Both devices now have each other's data
- The
Routermust be kept alive globally (ROUTERstatic) — dropping it stops incoming connections. - After sending the final SYNC_PUSH, call
send.finish()+ short delay so the server processes it before the connection drops. - Loro snapshots are stored in
Tree::LoroSnapshotskeyed bySubject::pure_id()(strips query params/drive hints). collect_drive_subjects()andbuild_drive_vvs()must usepure_id()consistently to match snapshot keys.
did:ad:node:<hex>— URI format for Iroh NodeIDs, used in QR codes and UI.- NodeIDs are persistent — derived from a secret key stored in redb (
Tree::PluginMeta). - Known peers are also stored in
Tree::PluginMetaas a JSON array.
TESTING_COVERAGE.md maps which flows are tested at
which layer, and — more usefully — which are not. Read it before deciding where
a new test belongs, and update it when you add one or discover a gap.
cargo test -p atomic_lib --no-default-features # 76 tests
cargo test -p atomic-server --lib # 23 tests
cargo test -p atomic-server --test sync # integration test: real server, 2 agents, WS sync
cargo test -p atomic_lib --features "iroh,discovery,db-redb" --lib -- sync::tests # Iroh sync tests (incl. live sync)
cargo test -p atomic_lib --features "iroh,db-redb" --lib -- sync::iroh_e2e -- --test-threads=1 # Iroh e2e: bulk + live + folderId
cargo test -p atomic_lib --features db-redb,iroh --test identity_durability # identity/peers survive an unclean kill
cargo test -p atomic_lib --features db-redb,iroh --test cross_process_sync # two OS processes reconcile over Iroh
cargo test -p atomic-server --test it iroh_pairing # two servers pair via POST /iroh-sync
cargo test --manifest-path flutter/rust/Cargo.toml # Flutter bridge (workspace-excluded, needs --manifest-path)
cd browser/lib && pnpm test # 29 JS tests
cd browser && pnpm run -r build # Full workspace build
cd browser && pnpm run test-e2e # Full e2e test
The startup update script only runs pnpm install (in browser/). Everything below is
already handled in the VM snapshot; these notes capture the non-obvious gotchas for
building/running the stack again after pulling changes.
The frontend's browser/data-browser/.env.development and the Vite proxy both point at
http://localhost:9885, but atomic-server defaults to 9883. For a standalone dev
setup the two MUST be aligned, so start the server on 9885:
/workspace/target/debug/atomic-server --port 9885 # subsequent runs
/workspace/target/debug/atomic-server --port 9885 --initialize # first run / to reset the /setup invite
If they disagree, the app silently repoints drives to a server that isn't listening and
auth/drive resolution fails. Then open http://localhost:6747/app/dev-drive for a clean
authenticated agent + drive.
data-browser's build:wasm uses CARGO_ENCODED_RUSTFLAGS=$'--cfg\x1f...' (bash ANSI-C
quoting). The VM's /bin/sh is dash, which doesn't understand $'...', so pnpm scripts
must run under bash. This is configured once (persisted in ~/.config/pnpm/rc):
pnpm config set script-shell /usr/bin/bash
If pnpm build:wasm ever fails with error: multiple input filenames provided (... $--cfg\x1f...),
re-run that config command.
browser/data-browser/public/wasm/{atomic_wasm.js,atomic_wasm_bg.wasm} are git-ignored and
must be generated (needs the wasm32-unknown-unknown target, already installed):
pnpm --filter @tomic/data-browser build:wasm
Only re-run this when the wasm/ or lib/ Rust changes; it is not part of pnpm start.
cd browser && pnpm start runs @tomic/lib + @tomic/react (tsup watch) and the Vite dev
server (:6747) together. During vite dev you'll see Compilation failed: <file> lines
from babel-plugin-react-compiler — these are non-fatal (the compiler skips auto-memoizing
files with try/catch); the app still serves and HMRs normally.
| Service | Dir | Dev command | Port |
|---|---|---|---|
| AtomicServer (Rust: HTTP/WS, redb, tantivy, Loro sync) | server/ |
cargo run -- --port 9885 |
9885 |
Frontend (Vite) + @tomic/lib/@tomic/react watch |
browser/ |
pnpm start |
6747 |