Documents REST & SQLite Persistence
chronicle beginner 4 min read
ELI5
Before two people can collaborate on a document, the document must exist as a row in SQLite. The relay’s REST API is the front door for that row: create it, list yours, archive it, soft-delete it. The “real” content lives in operations; this table just owns identity, ownership, and status.
Technical Deep Dive
Storage Decisions (ADR-003)
| Concern | Choice |
|---|---|
| Database | SQLite (embedded, serverless) |
| Driver | sqlx 0.8 |
| ID strategy | UUIDv7 (time-ordered) stored as TEXT |
| Timestamps | ISO 8601 strings (TEXT columns) |
| Deletion | Soft delete (status = 'deleted', deleted_at set) |
Document Model
classDiagram class Document { +Uuid id +String title +Option~String~ description +String owner_id +DocumentStatus status +DateTime~Utc~ created_at +DateTime~Utc~ updated_at +Option~DateTime~Utc~~ deleted_at } class DocumentStatus { <<enum>> Active Archived Deleted } class DocumentRepository { -SqlitePool pool +create(title, owner_id) Document +get(id) Document "active only" +get_any(id) Document "any status" +list_by_owner(owner_id) Vec~DocumentSummary~ +update(id, request) Document +touch(id) () +delete(id) () "soft" +is_owner(doc, user_id) bool } Document --> DocumentStatus DocumentRepository --> DocumentConfiguration
| Env var | Default |
|---|---|
RELAY_DATABASE_URL | sqlite:/app/data/chronicle.db?mode=rwc (Docker-only path; override locally) |
RELAY_PORT | 8080 |
RELAY_HOST | 0.0.0.0 |
AppState::new runs documents.initialize() then operations.initialize() on startup (relay/src/services/state.rs); both apply their migration files via sqlx::raw_sql.
Create + Edit Round-Trip
sequenceDiagram autonumber participant Client as app (api.ts) participant API as /api/documents (handlers/documents.rs) participant Repo as DocumentRepository participant DB as SQLite participant WS Client->>API: POST /api/documents { title } API->>Repo: create(title, owner_id from auth) Repo->>DB: INSERT INTO documents ... id = uuidv7() DB-->>Repo: ok Repo-->>API: Document API-->>Client: 201 { id, ... } Client->>WS: ws://.../ws/documents/:id WS->>WS: authenticate + join_document WS->>Repo: get(id) (verify exists, active) WS-->>Client: joined_document { current_version, user_count }Indexes That Matter for Listing
| Index | Query |
|---|---|
idx_documents_owner_id | ”list my documents” |
idx_documents_status | filter active vs archived |
idx_documents_owner_active (partial) | most common combined query |
idx_documents_updated_at DESC | recency sort |
Health & Uptime
AppState::check_database runs SELECT 1; uptime_seconds returns started_at.elapsed().as_secs(). Both exposed via the /api/health mount in main.rs.
Key Terms
- DocumentSummary → reduced projection used in list responses; avoids streaming the full description for many rows.
- soft delete → row remains,
status = 'deleted',deleted_atpopulated;getreturns 404 butget_anystill finds it (audit + recovery). - touch → bumps
updated_atonly; called when an op lands so list ordering by recency reflects activity, not metadata edits.
Q&A
Q: Why UUIDv7 instead of UUIDv4?
A: v7 is time-ordered, so a B-tree index on id reflects creation order — cheap recency sorts without hitting created_at.
Q: Why no foreign key from operations.document_id to documents.id?
A: The migration leaves the FK commented; foreign_keys = on is set on the connection pool, but the constraint is omitted to keep the operations table independently creatable in tests. ADR-003 calls this out.
Q: What error does opening a missing local SQLite path produce?
A: With mode=rwc SQLx tries to create the file; on a non-existent parent directory it fails — set RELAY_DATABASE_URL=sqlite:./chronicle.db?mode=rwc for local dev.
Examples
A fresh integration test sets RELAY_DATABASE_URL=sqlite::memory:, lets AppState::new run both migrations, then creates a document and connects a WS — the same path production uses, with :memory: standing in for the on-disk file.
neighbors on the map
- Operations & Versions Schema writing a new sync query
- Two-Service Architecture onboarding to the chronicle-hq monorepo