commit f52439bb8de182f6348802994952ec99cf4fd6ca
parent e9293bae2480eb0a009d4342300d92a8990db357
Author: Silas Brack <silasbrack@gmail.com>
Date: Sat, 13 Jun 2026 14:56:02 +0200
docs: add migration guidelines to README
Documents transaction semantics, version bumping, PRAGMA handling,
and testing. Also updates auth description (opaque sessions, not JWT).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Diffstat:
| M | README.md | | | 73 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- |
1 file changed, 70 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
@@ -7,7 +7,7 @@ A link aggregator built with Rust, Axum, Askama, and SQLite.
- **Mechanical sympathy** — match concurrency to hardware. Thread-local SQLite connections, one per core. No mutex contention on the read path.
- **Simplicity** — no ORMs, no abstractions. SQL strings, direct function calls, linear control flow. A handler reads top-to-bottom: authenticate, query, render, respond.
- **Crash early** — all environment variables are required. Missing config panics on startup, not at request time. No silent defaults.
-- **Minimal dependencies** — hand-rolled SMTP, connection pool, and migration system. 9 direct dependencies.
+- **Minimal dependencies** — hand-rolled SMTP, connection pool, and migration system.
- **No indirection** — no traits "in case we swap later", no generic abstractions over concrete types.
## Architecture
@@ -15,7 +15,7 @@ A link aggregator built with Rust, Axum, Askama, and SQLite.
- **Axum** async web server with **Askama** compile-time HTML templates
- **SQLite** via rusqlite — embedded, no separate database service
- **Datastar** v1.0.2 for frontend interactivity (voting, search, real-time comment updates via SSE)
-- **Auth**: argon2id password hashing, JWT tokens, email verification via hand-rolled SMTP
+- **Auth**: argon2id password hashing, opaque session tokens, email verification via hand-rolled SMTP
### Database
@@ -23,7 +23,7 @@ A link aggregator built with Rust, Axum, Askama, and SQLite.
- Single writer connection behind a Mutex (SQLite allows one writer at a time)
- Blocking threads capped to core count via `max_blocking_threads`
- WAL mode, `synchronous=NORMAL`, mmap, prepared statement caching
-- BurntSushi-style migrations: numbered `.sql` files in `migrations/`, embedded at compile time via `include_str!`
+- All tables use STRICT mode
### Frontend (Datastar)
@@ -31,3 +31,70 @@ A link aggregator built with Rust, Axum, Askama, and SQLite.
- SSE responses use `text/event-stream` with `datastar-patch-elements` events
- Default outer morph mode — include element ID in response HTML
- `X-Accel-Buffering: no` header on SSE responses for nginx compatibility
+
+## Migrations
+
+Schema migrations live in `migrations/` as numbered SQL files, embedded at compile time via `include_str!`.
+
+```
+migrations/
+ 0001_initial_schema.sql
+ 0002_email_verification.sql
+ 0003_strict_tables.sql
+ 0004_sessions.sql
+```
+
+### Adding a migration
+
+1. Create `migrations/NNNN_description.sql`
+2. Add `include_str!("../migrations/NNNN_description.sql")` to `src/migrations.rs`
+
+### Migration file format
+
+Each migration file owns its own transaction and version bump:
+
+```sql
+-- PRAGMAs that must run outside a transaction go first
+PRAGMA foreign_keys=OFF;
+
+BEGIN IMMEDIATE;
+
+-- Schema changes here
+ALTER TABLE ...;
+CREATE TABLE ...;
+
+-- Bump the version (must match the migration number)
+UPDATE migration_version SET version = N;
+COMMIT;
+
+-- Restore PRAGMAs
+PRAGMA foreign_keys=ON;
+```
+
+Rules:
+- Every migration must `BEGIN IMMEDIATE` and `COMMIT`
+- The version bump (`UPDATE migration_version`) must be inside the transaction — if the migration fails, the version doesn't advance
+- 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`
+- Once a migration is committed to main, it must never change
+- Two branches adding migrations will conflict in `src/migrations.rs` (git catches it)
+
+### Testing
+
+`cargo test` verifies:
+- All migrations apply cleanly on a fresh database
+- Running migrations twice is a no-op (idempotent)
+- `PRAGMA foreign_keys` is ON after all migrations complete
+
+## Setup
+
+```bash
+cp .env.example .env # edit with real values
+nix develop
+cargo run
+```
+
+## Deployment
+
+Deployed on NixOS via an infrastructure flake. Push to `simple-web-app`, update the infra flake (`nix flake update simple-web-app`), push infra.
+
+Static assets (CSS, JS) are embedded in the binary at compile time. Nginx handles gzip compression.