CRUMB a card from devarno-cloud

Two-Service Architecture

chronicle beginner 4 min read

ELI5

Chronicle is a postal system with two buildings. The app/ building (Next.js) is the front desk where customers write letters. The relay/ building (Rust/Axum) is the sorting room that stamps every letter with a clock, files a copy, and shouts the contents into rooms where other customers are listening.

Technical Deep Dive

Repository Layout

PathStackRole
app/Next.js 14 App Router, Zustand+Immer, TanStack Query, Tailwind, VitestEditor UI, presence, timeline scrubbing
relay/Rust, Axum, tokio-tungstenite, sqlx, SQLite, jsonwebtokenAuth, CRDT op relay, persistence, timeline reconstruction
docs/adr/MarkdownADRs 001–006 (protocol, auth, persistence, CRDT, editor integration, timeline)

There is no workspace tooling — each service has its own package.json / Cargo.toml and CI.

Service Boundary

flowchart LR
subgraph Browser
UI[Next.js App<br/>app/src]
CRDT[CrdtDocument<br/>+ ClientHlc<br/>app/src/services]
end
subgraph Relay[Rust Relay]
HTTP["/api routes<br/>handlers/documents.rs"]
WS["/ws/documents/:id<br/>handlers/websocket.rs"]
AUTH[AuthService<br/>services/auth.rs]
OPS[OperationRepository<br/>services/operation.rs]
HLC[HlcClock<br/>services/hlc.rs]
end
DB[(SQLite<br/>chronicle.db)]
UI -- REST: documents CRUD --> HTTP
UI -- WebSocket JSON --> WS
WS --> AUTH
WS --> OPS
WS --> HLC
HTTP --> OPS
OPS --> DB
HTTP --> DB

Protocol Mirror

The wire protocol is mirrored in two languages and must stay in sync:

  • Rust source of truth: relay/src/protocol/ (schema.rs, error_codes.rs, validation.rs)
  • TypeScript mirror: app/src/services/protocol.ts

Any change to a message shape, error code, or PROTOCOL_VERSION lands in both files. See ADR-001.

Top-level Context

---
title: "Chronicle-HQ system context"
---
flowchart TD
user(("<b>Editor user</b><br/>Writes documents collaboratively")):::person
subgraph chronicle ["**Chronicle-HQ**"]
app["<b>app/ (Next.js)</b><br/>Editor + Timeline UI"]:::system
relay["<b>relay/ (Rust/Axum)</b><br/>Auth, CRDT relay, persistence"]:::system
sqlite[("<b>SQLite</b><br/>documents, operations, snapshots")]:::db
end
idp["<b>External Auth Provider</b><br/>Issues HS256 JWTs"]:::ext
user -- "HTTPS" --> app
app -- "REST /api + WebSocket /ws" --> relay
relay -- "sqlx (WAL mode)" --> sqlite
idp -- "Tokens" --> app
classDef person fill:#1c1c24,stroke:#e85d3e,color:#f0ece6
classDef system fill:#1c1c24,stroke:#d4a574,color:#f0ece6
classDef ext fill:#141419,stroke:#8b7e74,color:#f0ece6,stroke-dasharray: 4 3
classDef db fill:#1c1c24,stroke:#d4a574,color:#f0ece6
classDef container fill:#1c1c24,stroke:#d4a574,color:#f0ece6

Key Terms

  • relay → the Rust collaboration server in relay/ (the brand also reuses the word, but in code “relay” always means this service).
  • app → the Next.js frontend in app/; path alias @/*app/src/*.
  • PROTOCOL_VERSION → integer constant defined in both relay/src/protocol/mod.rs and app/src/services/protocol.ts; clients send it inside Authenticate.
  • mirror file → a TS or Rust file whose entire job is to keep wire types identical across services.

Q&A

Q: Why no workspace manager (turborepo / pnpm workspaces)? A: Per CLAUDE.md, each service has its own deps and CI by design — the only cross-cutting concern is the protocol mirror, which is two files.

Q: Where is the database physically stored at runtime? A: RELAY_DATABASE_URL defaults to sqlite:/app/data/chronicle.db?mode=rwc, a path that only exists inside the Docker image. Local dev must override.

Q: Does the app talk to SQLite directly? A: No. Only the relay opens the SQLite pool (relay/src/services/state.rs::AppState::new); the app reaches data via REST and WebSocket.

Examples

A “type a character” round-trip touches all three layers: editor diff (app/src/services/diff.ts) → CRDT op → WebSocket frame → handlers/websocket.rsOperationRepository::store → SQLite → broadcast back to every other connected client.

neighbors on the map