🥃 Bourbonacci

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.

Built with FastAPI, vanilla JS, and MySQL — fully containerized with Docker Compose. No framework dependencies on the frontend.

The Name

Bourbonacci is a blend of Bourbon and the Fibonacci sequence. The Fibonacci sequence — 1, 1, 2, 3, 5, 8, 13… — builds endlessly on everything that came before it, with no defined end. An infinity bottle works the same way: every addition layers on top of the existing blend, compounding in complexity over time. Just like the sequence, your bottle is never truly finished.

Features

  • Infinity bottle tracking — log add entries (bourbon name, proof, shots) and remove entries (shots consumed)
  • Live stats — bourbons added, total shots poured in, shots remaining, estimated weighted proof
  • Bottle fill bar — visual level indicator based on your configured bottle size
  • Community page — public leaderboard of all bottles; click any card to see a searchable, alphabetical bourbon list
  • Settings modal — update display name, bottle size, timezone, and password from any page via the gear icon
  • Admin panel — list all users, reset passwords, disable/enable accounts, delete users, and impersonate any user for debugging
  • JWT auth — Bearer token auth with impersonation support (admin tokens carry an admin_id claim)
  • About page — public explainer on what an infinity bottle is, why to track it, and how to get started

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
Auth JWT via python-jose, passwords hashed with passlib[bcrypt==4.0.1]
Container Docker Compose

Project Structure

bourbonacci/
├── backend/
│   ├── app/
│   │   ├── main.py           # FastAPI app entry point, lifespan (DB init + admin seed)
│   │   ├── config.py         # Pydantic settings (reads from .env)
│   │   ├── database.py       # Async engine, session factory, Base, init_db()
│   │   ├── dependencies.py   # get_db, get_current_user, get_current_admin
│   │   ├── 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/PUT /api/users/me, PUT /api/users/me/password
│   │   │   ├── entries.py    # CRUD /api/entries, GET /api/entries/stats
│   │   │   ├── public.py     # GET /api/public/stats (unauthenticated)
│   │   │   └── admin.py      # Admin user management + impersonation
│   │   ├── schemas/
│   │   │   ├── user.py       # Pydantic schemas for user/auth
│   │   │   └── entry.py      # Pydantic schemas for entries/stats
│   │   └── utils/
│   │       └── security.py   # JWT creation/decode, password hashing
│   ├── Dockerfile
│   └── requirements.txt
├── frontend/
│   ├── index.html            # Landing page + public community bottles
│   ├── about.html            # What is an infinity bottle? + Bourbonacci explainer
│   ├── login.html
│   ├── register.html
│   ├── dashboard.html        # My Bottle — stats, fill bar, entry log
│   ├── log.html              # Log add or remove entries
│   ├── profile.html          # Profile settings (display name, timezone, password)
│   ├── admin.html            # Admin user management panel
│   ├── css/style.css         # Bourbon-themed dark UI
│   └── js/
│       ├── api.js            # Fetch wrapper, token injection, escHtml, admin API
│       ├── auth.js           # Auth state, nav rendering, settings modal
│       └── admin.js          # Admin page logic (user table, modals, impersonation)
├── nginx/
│   └── default.conf          # Serves frontend, proxies /api/ to backend:8000
├── docker-compose.yml
└── .env.example

Pages

Page Auth Description
/index.html No Landing page with community bottle cards; click to browse bourbons
/about.html No What is an infinity bottle? Why Bourbonacci? How to get started
/login.html No Login form — redirects to /index.html on success
/register.html No Registration form
/dashboard.html Yes My Bottle — stats, fill bar, full entry log with delete
/log.html Yes Log an add or remove entry
/profile.html Yes Edit display name, timezone, bottle size, password
/admin.html Admin User management — reset PW, disable, impersonate, delete

Navigation

The top-right nav area shows (when logged in):

  • Display name — non-clickable label
  • Admin button — only visible to admin accounts, links to /admin.html
  • ⚙ Settings button — opens a modal to update display name, bottle size, timezone, and password
  • Logout button

When impersonating a user, the nav shows "Viewing as [name]" and a Return to Admin button.

API Endpoints

Auth

Method Path Auth Description
POST /api/auth/register No Register new account, returns JWT
POST /api/auth/login No Login, returns JWT

Users

Method Path Auth Description
GET /api/users/me Yes Get current user profile
PUT /api/users/me Yes Update display name, timezone, bottle size
PUT /api/users/me/password Yes Change password

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 Stats + bourbon list for all users

Admin

Method Path Auth Description
GET /api/admin/users Admin List all users
POST /api/admin/users Admin Create a user
POST /api/admin/users/{id}/reset-password Admin Force-reset a user's password
POST /api/admin/users/{id}/disable Admin Disable a user account
POST /api/admin/users/{id}/enable Admin Re-enable a user account
DELETE /api/admin/users/{id} Admin Hard-delete a user and all their data
POST /api/admin/users/{id}/impersonate Admin Get a token scoped as that user
POST /api/admin/unimpersonate Admin Exchange impersonation token back for admin token

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 Response

{
  "total_add_entries": 12,
  "total_shots_added": 18.5,
  "current_total_shots": 14.25,
  "estimated_proof": 95.3
}

current_total_shots = total added total removed. estimated_proof = weighted average proof across all add entries by shot count.

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(64))"

Run

docker compose up -d

The app will be available at http://localhost (or whichever port is mapped in docker-compose.yml).

The backend waits for MySQL to pass its healthcheck before starting. Tables are created automatically on first boot via SQLAlchemy's create_all. The admin account defined in .env is seeded/re-synced on every container start.

Stop

docker compose down
# To also remove the database volume:
docker compose down -v

Environment Variables

Variable Description
MYSQL_ROOT_PASSWORD MySQL root password
MYSQL_DATABASE Database name (default: bourbonacci)
MYSQL_USER App DB user
MYSQL_PASSWORD App DB password
DATABASE_URL SQLAlchemy async DSN (mysql+aiomysql://user:pass@db:3306/dbname)
SECRET_KEY JWT signing secret — keep long and random
ACCESS_TOKEN_EXPIRE_MINUTES JWT TTL in minutes (default: 480)
ADMIN_USERNAME Admin account email, seeded on every container start
ADMIN_PASSWORD Admin account password, re-synced on every container start

Data Model

users

Column Type Notes
id int PK
email varchar(255) unique
password_hash varchar(255) bcrypt
display_name varchar(100) nullable
timezone varchar(50) default UTC
bottle_size float nullable user-configured bottle capacity in shots
is_admin bool default false
is_disabled bool default false
created_at datetime server default

entries

Column Type Notes
id int PK
user_id int FK → users cascade delete
entry_type enum add or remove
date date user-supplied
bourbon_name varchar nullable required for add entries
proof float nullable
amount_shots float default 1.0
notes text nullable
created_at datetime server default
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.
Readme 129 KiB
Languages
HTML 41%
Python 26.6%
JavaScript 20.5%
CSS 11.7%
Dockerfile 0.2%