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.
Design System
Color Palette
Based on the Kanagawa color scheme.
Core Tokens
| Token | Value |
|---|---|
--bg-primary | Sumi Ink 3 |
--bg-secondary | Sumi Ink 2 |
--bg-elevated | Sumi Ink 0 |
--text-primary | Fuji White |
--text-secondary | Old White |
--accent-primary | Spring Blue |
--accent-secondary | Oni Violet |
Theme Variants
- Default (wave): Dark theme with blue/teal accents
- Dragon: Warmer dark theme with muted greens
- Lotus: Light theme with warm paper tones
Set the theme via data-theme attribute: data-theme="dragon" or data-theme="lotus".
Typography
Based on Bringhurst’s typographic scale (ratio 1.25).
Type Scale
| Token | Size |
|---|---|
--text-xs | 0.64rem |
--text-sm | 0.8rem |
--text-base | 1rem |
--text-lg | 1.25rem |
--text-xl | 1.563rem |
--text-2xl | 1.953rem |
--text-3xl | 2.441rem |
--text-4xl | 3.052rem |
Line Height
| Token | Value |
|---|---|
--leading-none | 1 |
--leading-tight | 1.25 |
--leading-normal | 1.5 |
--leading-loose | 2 |
Spacing
Modular spacing scale matching the type scale:
| Token | Size |
|---|---|
--space-xs | 0.5rem |
--space-sm | 0.75rem |
--space-md | 1rem |
--space-lg | 1.25rem |
--space-xl | 1.563rem |
--space-2xl | 1.953rem |
Usage
Apply tokens directly in CSS or SCSS:
.card {
background-color: var(--bg-secondary);
color: var(--text-primary);
padding: var(--space-md);
border: 1px solid var(--border-color);
}
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