# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Changed - **Role-based access control**: `user_roles` table with `role_required()` decorator replaces password-based admin auth - **Admin is a real user**: admins authenticate via magic links; `ADMIN_EMAILS` env var auto-grants admin role on login - **Separated billing entities**: `billing_customers` table holds payment provider identity; `subscriptions` table holds only subscription state - **Multiple subscriptions per user**: dropped UNIQUE constraint on `subscriptions.user_id`; `upsert_subscription` finds by `provider_subscription_id` ### Added - Simple A/B testing with `@ab_test` decorator and optional Umami `data-tag` integration (`UMAMI_SCRIPT_URL` / `UMAMI_WEBSITE_ID` env vars) - `user_roles` table and `grant_role()` / `revoke_role()` / `ensure_admin_role()` functions - `billing_customers` table and `upsert_billing_customer()` / `get_billing_customer()` functions - `role_required(*roles)` decorator in auth - `is_admin` template context variable - Migration `0001_roles_and_billing_customers.py` for existing databases ### Removed - `ADMIN_PASSWORD` env var and password-based admin login - `provider_customer_id` column from `subscriptions` table - `admin/templates/admin/login.html` ### Changed - **Provider-agnostic schema**: generic `provider_customer_id` / `provider_subscription_id` columns replace provider-prefixed names (`stripe_customer_id`, `paddle_customer_id`, `lemonsqueezy_customer_id`) — eliminates all Jinja conditionals from schema, SQL helpers, and route code - **Consolidated `subscription_required` decorator**: single implementation in `auth/routes.py` supporting both plan and status checks, reads from eager-loaded `g.subscription` (zero extra queries) - **Eager-loaded `g.subscription`**: `load_user` in `app.py` now fetches user + subscription in a single JOIN; available in all routes and templates via `g.subscription` ### Added - `transactions` table for recording payment/refund events with idempotent `record_transaction()` helper - Billing event hook system: `on_billing_event()` decorator and `_fire_hooks()` for domain code to react to subscription changes; errors are logged and never cause webhook 500s ### Removed - Duplicate `subscription_required` decorator from `billing/routes.py` (consolidated in `auth/routes.py`) - `get_user_with_subscription()` from `auth/routes.py` (replaced by eager-loaded `g.subscription`) ### Changed - **Email SDK migration**: replaced raw httpx calls with official `resend` SDK in `core.py` - Added `from_addr` parameter to `send_email()` for multi-address support - Added `EMAIL_ADDRESSES` dict for named sender addresses (transactional, etc.) - **Paddle SDK migration**: replaced raw httpx calls with official `paddle-python-sdk` in `billing/routes.py` - Checkout, manage, cancel routes now use typed SDK methods (`PaddleClient`, `CreateTransaction`) - Webhook verification uses SDK's `Verifier` instead of hand-rolled HMAC - Added `PADDLE_ENVIRONMENT` config for sandbox/production toggling - Added `_paddle_client()` helper factory - **Dependencies**: `resend` replaces `httpx` for email; `paddle-python-sdk` replaces `httpx` for Paddle billing; `httpx` now only included for LemonSqueezy projects - Worker `send_email` task handler now passes through `from_addr` ### Added - `scripts/setup_paddle.py` — CLI script to create Paddle products/prices programmatically (Paddle projects only) ### Changed - **Pico CSS → Tailwind CSS v4** — full design system migration across all templates - Standalone Tailwind CLI binary (no Node.js) with `make css-build` / `make css-watch` - Brand theme with component classes (`.btn`, `.card`, `.form-input`, `.table`, `.badge`, `.flash`, etc.) - Self-hosted Commit Mono font for monospace data display - Docker multi-stage build: CSS compiled in dedicated stage before Python build ### Removed - Pico CSS CDN dependency - `custom.css` (replaced by Tailwind `input.css` with `@layer components`) - JetBrains Mono font (replaced by self-hosted Commit Mono) ### Fixed - Admin template collision: namespaced admin templates under `admin/` subdirectory to prevent Quart's template loader from resolving auth's `login.html` or dashboard's `index.html` instead of admin's - Admin user detail: `stripe_customer_id` hardcoded regardless of payment provider — now uses provider-aware Copier conditional (Stripe/Paddle/LemonSqueezy) ### Added - Initial project scaffolded from quart_saas_boilerplate