simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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.