A lightweight baby tracking dashboard for the early days. Track feedings, diapers, and health with a simple, mobile-first interface.
Self-hosted, no accounts, no cloud — just a single SQLite database.
![]() |
![]() |
![]() |
![]() |
| Dashboard & timeline | Live feeding timer | Log a feeding | Log a diaper change |
- Diaper Tracking — Log pee/poop/both with quick-tap buttons
- Feeding Tracking — Breast (L/R) and bottle with amount in oz
- Breastfeeding Timer — Live timer with breast switching, persists across page refreshes, two-step end confirmation
- Medication & Temperature — Log meds with dosage, temps with F↔C conversion
- Dashboard — Today's summary, last activity timestamps, 3-day activity timeline
- Edit & Delete — Tap any timeline entry to edit or remove it
- Light/Dark Mode — Follows system preference with manual toggle
- Data Export — CSV and JSON export of all records
- Mobile-First — Large touch targets, one-handed operation
- API Docs — Interactive OpenAPI docs at
/docs
Puffin is published as a container image on GitHub Container Registry.
docker run -d \
--name puffin \
-p 8000:8000 \
-v puffin-data:/data \
-e TZ=America/New_York \
ghcr.io/pid1/puffin:latestThen open http://localhost:8000.
/data— SQLite database (puffin.db). Mount this to persist data across container restarts.
PUFFIN_DB_PATH— Path to the SQLite database file (default:/data/puffin.db). Normally you don't need to change this.TZ— IANA timezone name (e.g.America/New_York) used to decide where one day ends and the next begins (default:UTC). Set this to your local timezone. Without it, an evening log west of UTC is counted as tomorrow — a diaper logged at 21:10 US Central lands on the next UTC day, so it won't show up in today's dashboard counts or timeline until midnight UTC. An unrecognized value falls back to UTC.PUFFIN_BACKUP_KEEP— How many database snapshots to retain per backup (default:10). Set to0to keep every snapshot.
All your data is a single SQLite file, so there are two safety nets:
-
Automatic pre-migration snapshots. On every startup, before any schema migration runs, the database is copied to
<db-dir>/backups/(i.e./data/backupsin Docker). Migrations rewrite tables in place, so this gives you a rollback point for the one operation most likely to damage data. Old snapshots are pruned toPUFFIN_BACKUP_KEEP. -
On-demand backups. Run
backup(indevenv shell) orpython -m puffin.backupto snapshot on demand. Wire it to cron or a systemd timer for regular copies, e.g. a daily line in the container host's crontab:0 3 * * * docker exec puffin python -m puffin.backup
To restore, stop the app and copy the chosen backups/*.db file back over puffin.db.
Off-box copies: these snapshots live on the same disk/volume as the database, so they protect against bad migrations and logical corruption but not against losing the disk. For disaster protection, periodically copy
backups/elsewhere, or replicate the database off-box with a tool like Litestream.
# Enter development environment
devenv shell
# Initialize (install dependencies)
setup
# Start development server
devThen open http://localhost:8000.
Run these commands inside devenv shell:
setup— Initialize the repo (install deps)dev— Start FastAPI dev server (port 8000)lint— Run ruff linterlint-fix— Run ruff linter with auto-fixformat— Run ruff formattertest— Run all tests (Python + JS)seed— Generate 14 days of realistic demo databackup— Snapshot the database into<db-dir>/backups
See AGENTS.md for the complete command reference including background/agent-friendly variants.
- Backend: Python 3.13 / FastAPI / SQLAlchemy / SQLite
- Frontend: Vanilla HTML/CSS/JS
- Dev Environment: Nix + devenv
- Package Manager: uv
- Linter/Formatter: ruff
- Tests: pytest with httpx TestClient
BSD 3-Clause. See LICENSE.



