916d638e84b622f37492ad01383bf7aa5d17bf62
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bourbonacci
A personal bourbon tracking web app. Log pours, track your collection, and view stats across users. Built with FastAPI, vanilla JS, and MySQL — fully containerized with Docker Compose.
Stack
| Layer | Technology |
|---|---|
| Backend | FastAPI (Python 3.12), async SQLAlchemy 2.0, Uvicorn |
| Database | MySQL 8 |
| Frontend | Vanilla HTML/CSS/JS (no framework) |
| Reverse Proxy | Nginx (Alpine) |
| Auth | JWT via python-jose, passwords hashed with passlib[bcrypt] |
| Container | Docker Compose |
Project Structure
bourbonacci/
├── backend/
│ ├── app/
│ │ ├── main.py # FastAPI app entry point, lifespan (DB init)
│ │ ├── config.py # Pydantic settings
│ │ ├── database.py # Async engine, session factory, Base
│ │ ├── dependencies.py # get_db, get_current_user
│ │ ├── models/
│ │ │ ├── user.py # User ORM model
│ │ │ └── entry.py # Entry ORM model (add/remove types)
│ │ ├── routers/
│ │ │ ├── auth.py # POST /api/auth/register, /api/auth/login
│ │ │ ├── users.py # GET/PATCH /api/users/me
│ │ │ ├── entries.py # CRUD /api/entries, GET /api/entries/stats
│ │ │ └── public.py # GET /api/public/stats (unauthenticated)
│ │ ├── schemas/
│ │ │ ├── user.py # Pydantic schemas for user/auth
│ │ │ └── entry.py # Pydantic schemas for entries/stats
│ │ └── utils/
│ │ └── security.py # JWT creation, password hashing/verification
│ ├── Dockerfile
│ └── requirements.txt
├── frontend/
│ ├── index.html # Landing / public stats page
│ ├── login.html
│ ├── register.html
│ ├── dashboard.html # Authenticated home with stats
│ ├── log.html # Log a new entry
│ ├── profile.html # User profile / settings
│ ├── css/style.css
│ └── js/
│ ├── api.js # Fetch wrapper, base URL, token handling
│ └── auth.js # Login/register/logout logic
├── nginx/
│ └── default.conf # Serves frontend, proxies /api/ to backend:8000
├── docker-compose.yml
└── .env.example
API Endpoints
Auth
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Register, returns JWT |
| POST | /api/auth/login |
No | Login, returns JWT |
Users
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/users/me |
Yes | Get current user profile |
| PATCH | /api/users/me |
Yes | Update display name / timezone |
Entries
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/entries |
Yes | List all entries for current user |
| POST | /api/entries |
Yes | Create an entry (add or remove) |
| DELETE | /api/entries/{id} |
Yes | Delete an entry |
| GET | /api/entries/stats |
Yes | Aggregated stats for current user |
Public
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/public/stats |
No | Aggregated stats for all users |
Authenticated routes expect Authorization: Bearer <token> header.
Entry Schema
{
"entry_type": "add" | "remove",
"date": "YYYY-MM-DD",
"bourbon_name": "string (required for add)",
"proof": 90.0,
"amount_shots": 1.0,
"notes": "optional string"
}
Stats return current_total_shots (adds minus removes) and estimated_proof (weighted average across add entries).
Getting Started
Prerequisites
- Docker + Docker Compose
Setup
cp .env.example .env
# Edit .env — set real passwords and a secure SECRET_KEY
Generate a secure secret key:
python3 -c "import secrets; print(secrets.token_hex(32))"
Run
docker compose up -d
The app will be available at http://localhost:8057.
The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's init_db().
Stop
docker compose down
# To also remove the database volume:
docker compose down -v
Environment Variables
| Variable | Description | Default |
|---|---|---|
MYSQL_ROOT_PASSWORD |
MySQL root password | — |
MYSQL_DATABASE |
Database name | bourbonacci |
MYSQL_USER |
App DB user | bourbonacci |
MYSQL_PASSWORD |
App DB password | — |
DATABASE_URL |
SQLAlchemy async DSN | mysql+aiomysql://... |
SECRET_KEY |
JWT signing secret (keep long & random) | — |
ACCESS_TOKEN_EXPIRE_MINUTES |
JWT TTL in minutes | 480 |
Data Model
users
id,email(unique),password_hash,display_name,timezone,created_at
entries
id,user_id(FK),entry_type(add/remove),date,bourbon_name,proof,amount_shots,notes,created_at
Description
A self-hosted infinity bottle tracker. Log every bourbon you add to your blend, track what you've consumed, and watch your bottle's story unfold over time. Community bottles are visible to anyone who visits the site — click a card to browse the full bourbon list inside that bottle.
Languages
HTML
41%
Python
26.6%
JavaScript
20.5%
CSS
11.7%
Dockerfile
0.2%