Introduction
Health Tracker is a personal health tracking application built with Leptos and Axum.
The problem
Health data is scattered across apps, devices, and spreadsheets. You want a single place to track your metrics, but existing tools are either too complex or too rigid.
The solution
A self-hosted web application that lets you log, visualize, and analyze your health data on your own terms. Built with Rust for performance and reliability, with a responsive Leptos frontend.
How it works
Health Tracker uses SSR + hydration for fast initial loads and smooth client-side interactivity:
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Browser │────▶│ Axum Server │────▶│ SQLite │
│ (Leptos) │◀────│ (SSR + API) │◀────│ (Storage) │
└─────────────┘ └──────────────┘ └─────────────┘
Key technologies
| Component | Technology |
|---|---|
| Framework | Leptos 0.8 (full-stack Rust) |
| Server | Axum |
| Build | cargo-leptos |
| Storage | SQLite (via sqlx) |
| Styling | SCSS |
| API | OpenAPI/Swagger (utoipa) |
| Validation | validator (derive) |
Architecture
Single-crate workspace with feature-gated compilation:
src/main.rs— Axum server entry (ssrfeature)src/lib.rs— WASM hydration entry (hydratefeature)src/app.rs— Shared components and routessrc/api/mod.rs— Leptos#[server]functions for UI-driven data flowsrc/server/api/— REST API for external consumerssrc/shared/— Framework-agnostic domain types and validationmigrations/— SQLx migration files
Next steps
- Quick Start: Get running in 5 minutes
- Architecture: Understand the system design
- Configuration: Customize settings
Quick start
Get the application running in 5 minutes.
Prerequisites
- Rust nightly (managed by
rust-toolchain.toml) cargo-leptos:cargo install cargo-leptos --locked
1. Clone and build
cargo leptos build
2. Run with hot-reload
cargo leptos watch
Or using just:
just serve
3. Open the app
Navigate to http://127.0.0.1:1337 in your browser.
Development workflow
just # Show available commands
just dev # Build in dev mode
just check # Format + clippy checks
just fix # Auto-fix formatting
just test # Run E2E tests
just ci # Simulate CI pipeline
Next steps
- Architecture: Understand the system design
- Configuration: Customize settings
- Development Setup: Set up your dev environment
Architecture
Health Tracker uses a single-crate workspace with feature-gated compilation for SSR + hydration.
System overview
┌─────────────────────────────────────────────────────────┐
│ Health Tracker │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ Browser │◀──▶│ Axum Server │◀──▶│ SQLite │ │
│ │ (Leptos) │ │ (SSR + API) │ │ (Storage) │ │
│ └─────────────┘ └──────────────┘ └────────────┘ │
│ │
│ Feature gates: │
│ - `hydrate` → WASM client bundle │
│ - `ssr` → Server binary │
│ - `cli` → Admin CLI binary (stub) │
│ - `gen` → Code generator binary (stub) │
└─────────────────────────────────────────────────────────┘
Crate structure
src/
├── main.rs # Axum server entry (ssr feature only)
├── lib.rs # WASM hydration entry (hydrate feature only)
├── app.rs # Shared root component + router (both features)
├── api/ # Leptos #[server] functions (client-server RPC)
│ └── mod.rs # upsert_daily_log, get_daily_log, get_daily_logs_for_month
├── routes/ # Page components
│ ├── index.rs # Landing page (/)
│ ├── dashboard.rs # Dashboard placeholder (/dashboard)
│ ├── track.rs # Daily log form (/track?date=YYYY-MM-DD)
│ ├── login.rs # Auth stub (/login)
│ ├── register.rs # Auth stub (/register)
│ └── docs.rs # Docs page (/docs)
├── components/ # Reusable UI
│ ├── header.rs # Navigation bar
│ ├── footer.rs # Footer with external links
│ ├── daily_log_form.rs # Full daily log form (4 sections)
│ └── layouts/
│ └── main_layout.rs # Header + Outlet + Footer wrapper
├── server/ # SSR-only (#[cfg(feature = "ssr")])
│ ├── db.rs # AppState with SqlitePool
│ ├── api/
│ │ └── daily_logs.rs # REST CRUD handlers + OpenAPI
│ └── queries/
│ └── daily_logs.rs # sqlx queries + 7 unit tests
├── shared/ # Framework-agnostic domain types
│ ├── models.rs # DailyLog, DailyLogSummary, ExerciseIntensity
│ └── validation.rs # DailyLogForm with #[derive(Validate)]
├── styles/
│ └── main.scss # SCSS styles
└── bin/
├── ht-cli.rs # Admin CLI (cli feature) — stub
└── ht-gen.rs # Code generator (gen feature) — stub
Feature gating
| Feature | Target | Purpose | Status |
|---|---|---|---|
hydrate | cdylib (WASM) | Client-side hydration | Complete |
ssr | bin (native) | Server-side rendering + API | Complete |
cli | bin (native) | Admin CLI binary | Stub |
gen | bin (native) | Code generator binary | Stub |
Both hydrate and ssr share app.rs, routes/, components/, and shared/ for consistency.
SSR + Hydration flow
- Server renders the initial HTML via
shell()inmain.rs - Browser receives HTML + WASM bundle
- Hydration takes over in
lib.rs, attaching reactivity to existing DOM - Client-side navigation via
leptos_routerfor subsequent pages
Data flow
The app has two parallel data paths:
Path 1: Leptos #[server] functions (UI-driven)
User Action → Form Signal → #[server] function → sqlx query → Database → Response → UI Update
Used by the DailyLogForm component for interactive form submission and loading existing data.
Defined in src/api/mod.rs:
upsert_daily_log(form)— create or update a logget_daily_log(date)— fetch a single logget_daily_logs_for_month(year, month)— calendar summaries
Path 2: REST API (external/CLI-driven)
HTTP Request → Axum handler → sqlx query → Database → JSON Response
Standard HTTP endpoints for CLI tools and external consumers. Swagger UI at /api/docs.
Defined in src/server/api/:
GET /api/daily_logs— list all logs (optional?start_date=and?end_date=)POST /api/daily_logs— create or update a logGET /api/daily_logs/{date}— get single log by dateDELETE /api/daily_logs/{date}— delete log by date
Module dependency rules
routes → components, api (#[server] functions), shared
components → shared (never server, never routes, never api)
server → shared (never components, never routes)
api → shared, server (queries)
shared → (nothing)
This ensures clean separation: domain types have no framework dependencies, server code knows nothing about UI, and components are framework-agnostic where possible.
Database
Single SQLite table daily_logs with columns for body metrics (weight, waist), nutrition (protein, calories), wellness (meditation, exercise + intensity), and sleep. Migrations run automatically on server startup via sqlx.
Dependencies
| Crate | Purpose |
|---|---|
leptos | Reactive UI framework |
leptos_router | Client-side routing |
leptos_meta | Document head management |
leptos_axum | SSR integration |
axum | HTTP server |
sqlx (sqlite, chrono, runtime-tokio-rustls) | Database queries |
validator (derive) | Input validation |
utoipa + utoipa-swagger-ui | OpenAPI spec + Swagger UI |
tokio | Async runtime |
tracing | Observability |
Configuration
Health Tracker is configured via Cargo.toml under [package.metadata.leptos].
Server settings
| Setting | Default | Description |
|---|---|---|
site-addr | 127.0.0.1:1337 | Server bind address |
reload-port | 1338 | Hot-reload WebSocket port |
Build settings
| Setting | Default | Description |
|---|---|---|
site-root | target/site | Output directory |
site-pkg-dir | pkg | Package subdirectory |
style-file | src/styles/main.scss | SCSS entry point |
assets-dir | public | Static assets |
Feature flags
| Feature | Description | Status |
|---|---|---|
ssr | Server-side rendering (Axum) | Complete |
hydrate | Client-side hydration (WASM) | Complete |
cli | Admin CLI binary | Stub |
gen | Code generator binary | Stub |
Environment variables
| Variable | Description |
|---|---|
DATABASE_URL | SQLite connection string (default: sqlite:data/health-tracker.db) |
LEPTOS_OUTPUT_NAME | Output binary name |
LEPTOS_SITE_ROOT | Site root directory |
LEPTOS_SITE_PKG_DIR | Package directory |
LEPTOS_SITE_ADDR | Server address |
LEPTOS_RELOAD_PORT | Reload port |
Lint configuration
Workspace-level clippy::pedantic is set to deny in Cargo.toml. All pedantic lints are warnings during iteration but must be resolved before committing.
Development Setup
Get your local development environment configured.
Prerequisites
- Rust nightly (managed by
rust-toolchain.toml) cargo-leptos:cargo install cargo-leptos --lockedlefthook:cargo install lefthook(for pre-commit hooks)commitizen:cargo install cargo-commitizen(for conventional commits)
Setup
# Install dependencies
cargo install cargo-leptos --locked
# Install git hooks
lefthook install
Running
# Development with hot-reload
just serve
# Build for development
just dev
# Build for release
just build
Code quality
# Run format and clippy checks
just check
# Auto-fix issues
just fix
# Run full quality suite
just quality
CI simulation
just ci
This runs the same checks as the Woodpecker CI pipeline locally.
Testing
Health Tracker uses two testing layers: unit tests for database queries and Playwright for end-to-end testing.
Unit tests
Server-side query tests live in src/server/queries/daily_logs.rs and use an in-memory SQLite database. They cover:
- Upsert (insert and update) operations
- Date-based retrieval
- Month summary generation
- Delete operations
Run with:
cargo test
E2E tests
End-to-end tests live in the end2end/tests/ directory and use Playwright to interact with the running application.
# Run E2E tests
just test
# Run E2E tests in release mode
just test-release
CI integration
Both unit tests and E2E tests run automatically in the Woodpecker CI pipeline. The pipeline order:
- lint —
cargo fmt --check,leptosfmt --check,cargo clippy -- -D warnings - test —
cargo test(unit tests) - build —
cargo leptos build --release
Architecture Decision Records
This directory contains Architecture Decision Records (ADRs) for health-tracker.
What is an ADR?
An ADR is a short document that captures an important architectural decision, along with its context and consequences.
Index
0001: Record architecture decisions
Date: 2026-05-18
Status
Accepted
Context
We need to record the architectural decisions made on this project.
Decision
We will use Architecture Decision Records, as described by Michael Nygard.
Consequences
- Every significant architectural decision should have an ADR
- ADRs are immutable once accepted (amendments create new ADRs)
- ADRs live in
docs/decisions/and are versioned with the code