Milestep is a Rust fullstack MVP for construction and modernization project planning, built from the supplied mockup.
- Backend: Rust, Axum, SQLx, PostgreSQL
- Frontend: Rust, Leptos CSR, WebAssembly, Trunk
- Shared contract: Serde DTO crate
- Deployment: Docker Compose with an internal Nginx asset/proxy container
Docker is not installed in the current environment, but the project includes Compose files.
cargo fmt --all
cargo check --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
Push-Location crates/frontend
trunk build --release
# Moves trunk's inline bootstrap into /init.js; required because the CSP
# does not allow inline scripts (use python3 instead of py on Linux/macOS).
py externalize-init.py dist
Pop-Location
cargo build --release -p milestep-backendThe backend refuses to start unless MILESTEP_SESSION_SECRET is set to at least 32 characters. Generate one and put it in .env:
# PowerShell
[Convert]::ToBase64String((1..48 | ForEach-Object { Get-Random -Maximum 256 }))# Linux/macOS
openssl rand -base64 48With Docker available:
Copy-Item .env.example .env
# Edit .env and set MILESTEP_SESSION_SECRET and POSTGRES_PASSWORD (see above)
docker compose up --buildPOSTGRES_PASSWORD is required as well; Compose refuses to start without it.
Docker Compose starts:
app: Axum API on the private Compose network.nginx: static WASM/assets plus/apireverse proxy, published onhttp://127.0.0.1:80by default.postgres: persistent PostgreSQL database, published on127.0.0.1:5432by default for local admin tools.
The app serves the API and frontend from one origin through Docker Nginx on http://127.0.0.1.
All published Compose ports bind to localhost only:
127.0.0.1:${MILESTEP_HTTP_PORT:-80}:80
127.0.0.1:${MILESTEP_POSTGRES_PORT:-5432}:5432Use deploy/nginx/external-http3-ubuntu.conf as the host-level Nginx example for Ubuntu 24.04 / 26.04.
It terminates TLS, enables HTTP/3 over QUIC on UDP 443, keeps HTTP/2 as TCP fallback, and proxies to Docker Nginx at 127.0.0.1:8080.
When running the host-level Nginx on the same machine, start Compose with a non-conflicting internal host port:
$env:MILESTEP_HTTP_PORT="8080"
docker compose up --buildBefore enabling it:
- Replace
milestep.example.com. - Replace the Let's Encrypt certificate paths.
- Verify HTTP/3 support with
nginx -V 2>&1 | grep -o -- '--with-http_v3_module'. - Open TCP
80, TCP443, and UDP443. - In production set
MILESTEP_COOKIE_SECURE=truefor theappservice (the session cookie then uses the__Host-prefix). - In production also set
MILESTEP_PUBLIC_ORIGIN=https://milestep.example.comso state-changing requests must carry exactly this Origin (CSRF hardening).
Use deploy/nginx/internal-vsphere-http3.conf when the app should run only on an
internal LAN address such as 192.168.1.x.
It binds Nginx to the VM LAN IP, allows only 192.168.1.0/24, enables HTTPS with
HTTP/3 over QUIC on UDP 443, keeps HTTP/2 as TCP fallback, and proxies to the
Docker Nginx published on 127.0.0.1:8080.
Verify that the host Nginx supports HTTP/3:
nginx -V 2>&1 | grep -o -- '--with-http_v3_module'Open TCP 80, TCP 443, and UDP 443 from the internal LAN.
For this setup, start Compose with:
$env:MILESTEP_HTTP_PORT="8080"
docker compose up -d --buildSet these production-style values in .env:
MILESTEP_COOKIE_SECURE=true
MILESTEP_PUBLIC_ORIGIN=https://planner.example.test- Login and register are rate limited per IP (10/min) both in nginx and in the backend; concurrent Argon2 hashing is bounded so auth floods cannot pin the CPU.
MILESTEP_TRUST_PROXY=true(set automatically in Compose) makes the backend useX-Real-IPfor rate limiting; never enable it without a trusted proxy in front. Compose isolates nginx and the app on a dedicatededgenetwork and trusts only that subnet (MILESTEP_TRUSTED_PROXIES=172.30.0.0/24), so postgres or any future sibling container cannot spoof the header to dodge the limit.- Workspace invites are single-use tokens with a 14-day expiry.
POST /api/workspaces/{id}/invitesreturnsinvite_tokenandinvite_path(/?invite=<token>); share that link out-of-band. Registering with the token joins the inviting workspace. If the invited email already has an account, it is added as a member directly and no token is returned. POST /api/auth/logout-allrevokes every session of the current user.- Sessions are stored hashed (SHA-256 of the cookie token), so a leaked database dump contains no usable sessions.
- Known trade-off: registering an already-taken email returns a 409, which reveals that the address has an account. Login (including its timing) stays enumeration-safe; closing the register side would need an email round trip, which this MVP does not have.
The demo seed is disabled by default. To try the demo, set MILESTEP_SEED_DEMO=true
(in .env or the environment) before the first start on an empty database:
- Email:
alex@firma.com - Password:
password123
Do not enable the seed in production: it creates seven well-known accounts with this public password.