README.md (3504B)
1 # Simple Web App 2 3 A link aggregator built with Rust, Axum, Askama, and SQLite. 4 5 ## Principles 6 7 - **Mechanical sympathy** — match concurrency to hardware. Thread-local SQLite connections, one per core. No mutex contention on the read path. 8 - **Simplicity** — no ORMs, no abstractions. SQL strings, direct function calls, linear control flow. A handler reads top-to-bottom: authenticate, query, render, respond. 9 - **Crash early** — all environment variables are required. Missing config panics on startup, not at request time. No silent defaults. 10 - **Minimal dependencies** — hand-rolled SMTP, connection pool, and migration system. 11 - **No indirection** — no traits "in case we swap later", no generic abstractions over concrete types. 12 13 ## Architecture 14 15 - **Axum** async web server with **Askama** compile-time HTML templates 16 - **SQLite** via rusqlite — embedded, no separate database service 17 - **Datastar** v1.0.2 for frontend interactivity (voting, search, real-time comment updates via SSE) 18 - **Auth**: argon2id password hashing, opaque session tokens, email verification via hand-rolled SMTP 19 20 ### Database 21 22 - Thread-local read connections (one per blocking thread, zero-contention) 23 - Single writer connection behind a Mutex (SQLite allows one writer at a time) 24 - Blocking threads capped to core count via `max_blocking_threads` 25 - WAL mode, `synchronous=NORMAL`, mmap, prepared statement caching 26 - All tables use STRICT mode 27 28 ### Frontend (Datastar) 29 30 - Use `data-init` (not `data-on:load`) for running expressions on element init 31 - SSE responses use `text/event-stream` with `datastar-patch-elements` events 32 - Default outer morph mode — include element ID in response HTML 33 - `X-Accel-Buffering: no` header on SSE responses for nginx compatibility 34 35 ## Migrations 36 37 Schema migrations live in `migrations/` as numbered SQL files, embedded at compile time via `include_str!`. 38 39 ### Adding a migration 40 41 1. Create `migrations/NNNN_description.sql` 42 2. Add `include_str!("../migrations/NNNN_description.sql")` to `src/migrations.rs` 43 44 ### Migration file format 45 46 Each migration file owns its own transaction and version bump: 47 48 ```sql 49 -- PRAGMAs that must run outside a transaction go first 50 PRAGMA foreign_keys=OFF; 51 52 BEGIN IMMEDIATE; 53 54 -- Schema changes here 55 ALTER TABLE ...; 56 CREATE TABLE ...; 57 58 -- Bump the version (must match the migration number) 59 UPDATE migration_version SET version = N; 60 COMMIT; 61 62 -- Restore PRAGMAs 63 PRAGMA foreign_keys=ON; 64 ``` 65 66 Rules: 67 - Every migration must `BEGIN IMMEDIATE` and `COMMIT` 68 - The version bump (`UPDATE migration_version`) must be inside the transaction — if the migration fails, the version doesn't advance 69 - PRAGMAs like `foreign_keys=OFF` go before `BEGIN` (they don't take effect inside transactions in some SQLite versions) and must be restored after `COMMIT` 70 - Once a migration is committed to main, it must never change 71 - Two branches adding migrations will conflict in `src/migrations.rs` (git catches it) 72 73 ### Testing 74 75 `cargo test` verifies: 76 - All migrations apply cleanly on a fresh database 77 - Running migrations twice is a no-op (idempotent) 78 - `PRAGMA foreign_keys` is ON after all migrations complete 79 80 ## Setup 81 82 ```bash 83 cp .env.example .env # edit with real values 84 nix develop 85 cargo run 86 ``` 87 88 ## Deployment 89 90 Deployed on NixOS via an infrastructure flake. Push to `simple-web-app`, update the infra flake (`nix flake update simple-web-app`), push infra. 91 92 Static assets (CSS, JS) are embedded in the binary at compile time. Nginx handles gzip compression.