derekc acfa21ec38 Update README to reflect current state
Add rate limits, pagination, Ntfy events, ALLOWED_ORIGINS, security
section, notify.py in project structure, corrected admin seed behaviour,
and updated stack versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 01:23:47 -07:00
2026-03-24 19:11:00 -07:00

🥃 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
  • Push notifications — Ntfy alerts for admin logins, new registrations, account changes, and service startup

Stack

Layer Technology
Backend FastAPI (Python 3.12), async SQLAlchemy 2.0.50, Uvicorn
Database MySQL 8
Frontend Vanilla HTML/CSS/JS (no framework)
Reverse Proxy Nginx
Auth JWT via python-jose, passwords hashed with passlib[bcrypt]
Rate limiting slowapi
Notifications ntfy (optional)
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
│   │   ├── limiter.py        # slowapi rate limiter instance
│   │   ├── 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
│   │       └── notify.py     # Ntfy push notification helper
│   ├── 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 Rate limit Description
POST /api/auth/register No 5/min Register new account, returns JWT
POST /api/auth/login No 10/min Login, returns JWT

Users

Method Path Auth Rate limit 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 5/min Change password

Entries

Method Path Auth Description
GET /api/entries Yes List entries for current user (?limit=200&offset=0, max 1000)
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 Rate limit Description
GET /api/public/stats No 30/min Stats + bourbon list for all users (max 500)

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
  • (Optional) Nginx Proxy Manager or equivalent for TLS termination

Setup

cp .env.example .env
# Edit .env — set real passwords, a secure SECRET_KEY, and your domain

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:8057 by default (configure the port mapping 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 on first boot — subsequent restarts will not overwrite a manually changed password.

Stop

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

Environment Variables

Variable Required Description
MYSQL_ROOT_PASSWORD Yes MySQL root password
MYSQL_DATABASE Yes Database name (default: bourbonacci)
MYSQL_USER Yes App DB user
MYSQL_PASSWORD Yes App DB password
DATABASE_URL Yes SQLAlchemy async DSN (mysql+aiomysql://user:pass@db:3306/dbname)
SECRET_KEY Yes JWT signing secret — keep long and random
ACCESS_TOKEN_EXPIRE_MINUTES No JWT TTL in minutes (default: 480)
ADMIN_USERNAME Yes Admin account email, seeded on first boot
ADMIN_PASSWORD Yes Admin account password, seeded on first boot
ALLOWED_ORIGINS No Comma-separated CORS origins (default: http://localhost)
NTFY_URL No Full Ntfy topic URL (e.g. https://ntfy.example.com/my-topic). Leave blank to disable notifications.
NTFY_TOKEN No Ntfy access token for protected topics

Ntfy Notifications

When NTFY_URL is set, the following events trigger push notifications:

Event Priority
Service startup Low
New user registration Default
Admin login High
User account disabled Default
User account deleted Default

Security

  • Passwords hashed with bcrypt
  • JWTs signed with HS256, configurable TTL
  • Rate limiting on auth and password change endpoints
  • Disabled accounts blocked at login and on every authenticated request
  • All security headers set in Nginx (HSTS, CSP, X-Frame-Options, etc.)
  • Gzip compression enabled for text responses
  • Backend runs as a non-root user inside the container
  • Container resource limits enforced (memory + CPU)
  • Docker image digests pinned in docker-compose.yml

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
S
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 169 KiB
Languages
HTML 40%
Python 28.8%
JavaScript 19.8%
CSS 11.1%
Dockerfile 0.3%