Personal blog, rewritten in Rust (axum). Server-rendered HTML with htmx for progressive enhancement, a repository-trait admin panel, PostgreSQL in production (SQLite in dev/tests).
The original FastAPI/Python implementation was migrated to Rust in a staged
rewrite — see plan.md for the migration plan and
MIGRATION_CONTRACT.md for the frozen HTTP/DB/admin
contract. TASKS.md tracks the remaining work.
rust/ Cargo workspace — the application
crates/domain/ Pure types + logic (serde structs, markdown, teaser, bcrypt,
repository traits). FROZEN contract — do not change public APIs.
crates/persistence/ SeaORM entities, baseline migration, repository trait impls
crates/web/ Axum app: routes, services, templates (Minijinja), admin, auth
crates/server/ Wiring binary: reads DATABASE_URL, applies migrations, serves
app/static/ esbuild output + sources (CSS/img/upload) — served at /static
deploy/ systemd unit + production cutover runbook (CUTOVER.md)
scripts/parity/ Python-vs-Rust parity harness (archived; golden results in results.md)
.github/workflows/ rust-ci.yaml (fmt/clippy/test/postgres-parity/browser-e2e), deploy.yaml
route (axum handlers) → service (structs) → repository (async traits) → entity (SeaORM)
↑ domain (serde structs, pure logic) crosses boundaries
- Web: Axum + tower-http (static files) on Tokio.
- ORM/DB: SeaORM (SQLx underneath); PostgreSQL in prod, SQLite in dev/tests.
- Templates: Minijinja — 16 templates ported from Jinja2; htmx dual-mode
rendering (full page vs fragment based on the
HX-Requestheader). - Auth: JWT in a signed
sessioncookie; bcrypt password hashes (existing hashes keep verifying — do not switch to argon2 without a re-hash migration). - Cache: moka (in-memory TTL, 50s,
"blog"namespace); admin writes invalidate it. Single-process, so no cross-worker staleness. - Markdown: comrak for post pages;
POST /md/uses a python-markdown- compatible preview renderer (see MIGRATION_CONTRACT.md for the residual differences). - Frontend: esbuild CSS pipeline (single dep); output served from
app/static/dist.
- Rust 1.96 (pinned in CI; rustfmt output is version-sensitive)
- Node + npm (only for
make css-build) - Docker (optional — Postgres parity tests via testcontainers)
make css-build # esbuild CSS (app/static/dist)
cd rust
DATABASE_URL='sqlite:///tmp/gunlinux-dev.db?mode=rwc' cargo run -p server
# or: make rust-runNotes:
sqlite://URLs need?mode=rwc— sqlx cannot create a missing file.- The server applies the baseline migration on startup (also on PostgreSQL; the production cutover stamps it rather than re-running schema creation).
- The dev default
sqlite://./tmp/dev.dbonly works because the file exists.
| Var | Default | Used by |
|---|---|---|
DATABASE_URL |
sqlite://./tmp/dev.db |
server (DB connection) |
BIND_ADDR |
0.0.0.0:8000 |
server (listen address) |
STATIC_DIR |
app/static |
web (static file root) |
SECRET_KEY |
dev-only default | web (session cookie signing) |
ENV |
development |
web settings |
YANDEX_VERIFICATION |
— | web settings (search-console meta tag) |
JWT_ALGORITHM / JWT_EXPIRE_MINUTES |
HS256 / 1440 |
web auth |
RUST_LOG |
info |
tracing |
make check # fmt + clippy + full workspace tests
cd rust && cargo test --workspace # default suite (SQLite)Feature-gated suites (default cargo test never compiles them):
cargo test -p persistence --features postgres-parity— the same repository suite against real PostgreSQL (testcontainers locally, or setTEST_DATABASE_URLfor CI). Catches SQLite↔Postgres divergence.cargo test -p web --features browser-tests --test test_browser— real headless-Chrome htmx swap tests (needs a browser; downloads one on demand).
- Image:
docker build -f rust/Dockerfile -t gunlinux-rust .(multi-stage; requiresapp/static/distfrommake css-build). - Deploy script:
.github/deploy.sh(builds the image on the server, installs the systemd unitgunlinux-ru, swaps off the legacy service). - Production cutover: follow
deploy/CUTOVER.md(backup → build/install → baseline stamp → smoke → rollback). The cutover commit ships the deploy script + CI trigger swap together.
6 tables: users, posts, categories, tags, posts_tags (m2m), icons.
Single SeaORM baseline migration m20260101_000001_create_schema (the 16
Alembic revisions it replaces); CREATE TABLE IF NOT EXISTS — non-destructive.
MIGRATION_CONTRACT.md— the frozen HTTP/htmx/DB/ admin contract (routes, status codes, bodies, schema, auth).scripts/parity/results.md— final Python-vs- Rust comparison: 18/19 status MATCH (one documented admin-root deviation), 16/19 normalized-body MATCH (remaining DIFFs are documented Thread-B admin differences and a whitespace-only markdown quirk).