This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Quantum of Thought is a note capture tool with real-time synchronization across multiple clients. The architecture uses CRDT (Conflict-free Replicated Data Types) via Automerge for distributed note editing without conflicts.
The system consists of four main components:
-
crdt_note (Rust/WASM): Core CRDT note implementation
- Uses Automerge for conflict-free merging
- Compiled to WASM for web client usage
- Also used directly by CLI client
- Each note has a UUIDv7 id and text content
-
client_web (React + TypeScript + Vite): Web interface
- Uses the CRDT WASM module via
crdt_notepackage - Three-layer data architecture:
- NoteService: Synchronizes notes across storage and networks (HTTP + WebSocket)
- NoteStore: React-aware external store using useSyncExternalStore API
- React Components: UI layer consuming NoteStore
- All public APIs are synchronous; async operations notify listeners when complete
- Uses localForage for client-side persistence
- Supports multiple network transports simultaneously
- Uses the CRDT WASM module via
-
client_cli (Rust): Command-line interface
- Binary name:
qot - Uses the crdt_note library directly (not WASM)
- Provides local note management
- Binary name:
-
server_web (Elixir/Phoenix): Backend server
- Authentication: Passwordless magic links with JWT access tokens (1hr) and refresh tokens (30 days)
- Main contexts:
Qot.Accounts- user management, magic link auth, token operationsQot.Notes- note operations with per-user isolation
- Per-user note isolation: All notes scoped by user_id, WebSocket channels are
notes:user:#{user_id} - Storage adapter pattern:
Qot.Storage.Adapterbehavior (currently ETS with composite keys{user_id, note_id}) - PubSub architecture: All note operations broadcast to per-user topics
notes:user:#{user_id} - WebSocket authentication: JWT token required in socket connect params, verified before channel join
Build WASM package (required before web client can use it):
cd crdt_note
wasm-pack build .This creates crdt_note/pkg/ directory which is consumed by client_web as a file dependency.
Run Rust tests:
cd crdt_note
cargo testInstall dependencies (includes the WASM package from ../crdt_note/pkg):
cd client_web
npm installDevelopment server:
cd client_web
npm run devBuild:
cd client_web
npm run buildLint:
cd client_web
npm run lintRun unit tests (Vitest):
cd client_web
npm testRun e2e tests (Playwright):
cd client_web
npm run test:e2eRun e2e tests with UI:
cd client_web
npm run test:e2e:uiDebug e2e tests:
cd client_web
npm run test:e2e:debugInstall locally:
cd client_cli
cargo install --path .This installs the qot binary.
Run tests:
cd client_cli
cargo testEnvironment Setup:
Uses mise for tool and environment management. Required environment variables in mise.local.toml:
DATABASE_URL- PostgreSQL connection (e.g., postgresql://localhost/qot_dev)FRONTEND_URL- Frontend URL for magic links (e.g., http://localhost:5173)JWT_SECRET_KEY- Secret key for signing JWT tokensRESEND_API_KEY- API key from resend.com for sending emails
Get dependencies:
cd server_web
mix deps.getRun database migrations:
cd server_web
mix ecto.migrateStart server in interactive mode:
cd server_web
iex -S mix phx.serverRun all tests:
cd server_web
mix testRun specific test file:
cd server_web
mix test test/path/to/test_file.exsRun specific test at line:
cd server_web
mix test test/path/to/test_file.exs:42- The web client uses
vite-plugin-wasmandvite-plugin-top-level-awaitfor WASM support - WASM boundary: Pass primitives and use non-mutable borrows to avoid gotchas
- The WASM package is linked as a file dependency in
client_web/package.json:"crdt_note": "file:../crdt_note/pkg"
- Web client supports multiple simultaneous network transports (HTTP + WebSocket)
- NoteService handles incoming messages from all networks and deduplicates
- All CRUD operations broadcast to all configured networks
- Server uses Phoenix PubSub to broadcast changes to all connected WebSocket clients
- Note data is serialized as Automerge binary format and transmitted as base64 over WebSocket
- User action → Component
- Component calls NoteStore method
- NoteStore calls NoteService method
- NoteService:
- Updates in-memory WASM note
- Persists to localForage
- Broadcasts to all networks (HTTP + WebSocket)
- Notifies listeners
- NoteStore updates snapshot and notifies React
- React re-renders components
- Passwordless authentication: Magic links sent via email (15 min expiry)
- Token architecture:
- Access tokens: JWT, 1 hour expiry, contain user_id claim
- Refresh tokens: Random, 30 days expiry, hashed in database
- Magic link tokens: Random, 15 min expiry, hashed in database
- Auth flow:
- User requests magic link → stored in
magic_link_tokenstable - User clicks link → creates/finds user, issues JWT + refresh token
- JWT used for API/WebSocket auth, refresh token for renewal
- User requests magic link → stored in
- WebSocket auth: JWT passed in socket connect params, user_id extracted and assigned to socket
- HTTP auth:
RequireAuthplug checks Bearer token, assigns user_id to conn
- All storage operations require
user_idparameter - ETS keys are composite:
{user_id, note_id} - PubSub topics are per-user:
"notes:user:#{user_id}" - WebSocket channels enforce user matching:
"notes:user:#{user_id}"only joinable by that user - Notes context filters all queries by user_id
Qot.Storage.Adapterdefines the behavior with user_id in all callbacks- Current implementation:
Qot.Storage.EtsAdapter(in-memory with composite keys) - Configured via
config/config.exs:config :qot, :storage_adapter, Qot.Storage.EtsAdapter - All storage operations return
{:ok, result}or{:error, reason}tuples
- Uses Ecto.Adapters.SQL.Sandbox for test database isolation
- Test email adapter:
Swoosh.Adapters.Testfor magic link emails - Test helpers in
test/support/:DataCase- sets up SQL Sandbox for all database testsConnCase- HTTP request testing with SQL SandboxChannelCase- WebSocket channel testing with SQL SandboxAuthHelpers- creates authenticated users with JWT tokens
- ETS table requires manual cleanup in tests:
:ets.delete_all_objects(:qot_notes) - Mix automatically sets
MIX_ENV=testwhen runningmix test