Axum Template - Portfolio/blog website template
Portfolio/blog website template for a company that does software/games projects
This README is the canonical docs — src/lib.rs pulls it in with #![doc = include_str!("../README.md")].
This template provides:
- Axum server with middleware
- Askama templates
- Containerization (with compose)
- Portfolio projects management
- Blog with markdown support
- Admin panel with authentication
- SQLite backend (default)
- SurrealDB backend (optional, behind feature flag)
- RBAC (User/Editor/Admin)
- HttpOnly cookie support
- Rate limiting on login
- Enable HTTPS
- Add login page template
- Audit logging
- Secure cookie flag
- Password reset flow
- Remember me checkbox with longer token TTL
- Simple footer with socials
- Team members: add and edit, with a flexible list of social links (platform picker, icons on the About page) and a drag/zoom avatar framer
- Image fields everywhere: paste a URL or upload, with a preview (team photo, post cover, project thumbnail and gallery)
Existing databases upgrade themselves on startup: SQLite gets the new team columns and carries the old GitHub/Twitter/LinkedIn URLs over as links; SurrealDB reads old records' URLs as links until they are next saved.
Install cargo-generate and run:
cargo generate olekspickle/axum-template -n my-project# SQLite3 backend:
just run
# SurrealDB backend
just run-surrealYou can peek into justfile for build details
The compiled Tailwind stylesheet (static/css/tailwind.css) is committed, so the site works out of the box.
After editing classes in templates/ or static/js/, rebuild it with just css (requires node).
Edit config.toml to configure:
- Server host/port
- Database path
- Admin credentials (password is argon2 hashed)
- Site name and tagline
- Set the
ADMIN_PASSWORDenv var — it is the only password source; the server refuses to start without it. (just run/just testuse a throwaway dev default.) With the SurrealDB backend, setSURREAL_PASSWORDthe same way. - Set
server.trusted_proxy = trueonly behind a proxy you control (nginx, cloudflared). It makes login rate limiting trustX-Real-IP/X-Forwarded-For; with it on and nothing in front, clients can pick their own rate limit bucket. - Keep
posts.db,cert.pemandkey.pemout of git. The database holds password hashes and session records. - Provide
cert.pem/key.pemto serve HTTPS. Their presence also flips the auth cookie toSecureand enables HSTS. - Password reset tokens are never returned in the HTTP response. Wire an email sender into
forgot_password; debug builds log the reset link to the server console for local testing.
Routes resolve against the role in the session token:
User(any authenticated team member) - read-only API endpoints.Editor- the mutating API endpoints as well.Admin- the admin panel, uploads and the Swagger UI / OpenAPI schema.
A team member's role comes from their role field; anything that isn't Editor or Admin
is treated as User.
POST /admin/upload accepts image and video files only (no SVG or HTML, both of which can
execute script from this origin), and the slug field is reduced to a safe filename token,
so uploads cannot escape static/media/.
just test # sqlite backend + unit tests
just test-surreal # starts a throwaway SurrealDB in docker and runs the backend round-tripsThe SurrealDB tests skip themselves when no server is reachable; point them elsewhere with
SURREAL_TEST_URL.
I found axum to be the most ergonomic web framework out there, and while there might be not enough examples at the moment, it is quite a breeze to use
- static files was sure one noticeable pain in the rear to figure out
- surrealdb sure adds complexity, I'm adding it under a feature because sqlite integration is so much less crates to compile(190+ vs 500+)
-
Cross-compile for Pi (aarch64):
Option A — native toolchain:
rustup target add aarch64-unknown-linux-gnu sudo apt install gcc-aarch64-linux-gnu # Debian/Ubuntu just build-piOption B — Docker-based (no toolchain to install):
cargo install cross just cross-build-pi
-
Copy to Pi:
rsync -avz target/aarch64-unknown-linux-gnu/release/axum-template \ config.toml static/ templates/ systemd/ \ pi@raspberrypi:~/deploy/ -
Run automated setup on Pi:
ssh pi@raspberrypi cd ~/deploy bash systemd/setup.sh
The script creates a system user, downloads cloudflared to
/opt/axum-template/cf/, creates symlinks at/usr/local/bin/cloudflaredand/usr/local/bin/cf, and installs the app, config and systemd services. Then it handles the Cloudflare Tunnel itself: browser login (once), tunnel create-or-reuse (idempotent — name collisions between duplicate tunnels are detected and cleaned up by ID), credentials install, and DNS routing. SetADMIN_PASSWORDin/etc/axum-template.envbefore starting the service. Undo everything withbash systemd/cleanup.sh. -
Tunnel knobs (all optional, sane defaults):
# Route DNS to a specific hostname (asked interactively if unset on a tty) CF_HOSTNAME=www.example.com bash systemd/setup.sh # Custom tunnel name / skip tunnel setup entirely CF_TUNNEL_NAME=mysite CF_SKIP_TUNNEL=1 bash systemd/setup.sh
If DNS routing fails with code 1003, a stale A/AAAA/CNAME record for that hostname already exists — delete it in the dashboard (DNS → Records) and re-run, or use an unused subdomain.
re-running the scriptis safe: it reuses the existing tunnel and patchesconfig.yml+ the service file to match.
Systemd service files are in systemd/
License: MIT OR Apache-2.0
