# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## What This Is A **Copier template** for generating SaaS applications. The template lives in `{{project_slug}}/` with Jinja2-templated files (`.jinja` extension). Files without `.jinja` are copied as-is. The `copier.yml` defines template variables: `project_slug`, `project_name`, `description`, `author_name`, `author_email`, `base_url`, `payment_provider` (stripe/paddle/lemonsqueezy). This is NOT a runnable application itself — it generates one via `copier copy`. ## Generated Project Commands After generation, the project uses **uv** as package manager: ```bash uv sync # Install dependencies uv run python -m .migrations.migrate # Initialize/migrate DB uv run python -m .app # Run dev server (port 5000) uv run python -m .worker # Run background worker uv run python -m .worker scheduler # Run periodic scheduler uv run pytest # Run tests uv run ruff check . # Lint docker compose up -d # Production deploy ``` ## Stack - **Quart** (async Flask) with Jinja2 templates and Pico CSS (no build step) - **SQLite** with WAL mode + aiosqlite (no ORM — plain SQL everywhere) - **Stripe**, **Paddle**, or **LemonSqueezy** for billing (chosen via `payment_provider` template variable) - **Resend** for transactional email - **Litestream** for SQLite replication/backups - **Docker + Caddy** for deployment - **Hypercorn** as ASGI server in production ## Architecture ### Domain-based flat structure Each domain is a directory with `routes.py` + `templates/`. Routes, SQL queries, and decorators all live together in `routes.py` — no separate models/services/repositories layers. ``` src// app.py → Application factory, blueprint registration, middleware core.py → Config class, DB helpers (fetch_one/fetch_all/execute), email, CSRF, rate limiting worker.py → SQLite-based background task queue (no Redis) auth/ → Magic link auth, login_required/subscription_required decorators billing/ → Checkout/webhooks/portal (Stripe, Paddle, or LemonSqueezy), plan feature/limit checks dashboard/ → User settings, API key management public/ → Marketing pages (landing, terms, privacy) api/ → REST API with Bearer token auth (api_key_required decorator) admin/ → Password-protected admin panel (ADMIN_PASSWORD env var) migrations/ → schema.sql + migrate.py (runs full schema idempotently with CREATE IF NOT EXISTS) ``` ### Key patterns - **Database access**: Use `fetch_one()`, `fetch_all()`, `execute()` from `core.py`. No ORM. Queries live directly in route files next to the routes that use them. - **Auth decorators**: `@login_required` for user pages, `@subscription_required(plans=["pro"])` for plan-gated features, `@api_key_required(scopes=["read"])` for API endpoints. - **CSRF**: `@csrf_protect` decorator on POST routes. Templates include ``. - **Background tasks**: `from ..worker import enqueue` then `await enqueue("task_name", {"key": "value"})`. Register handlers with `@task("task_name")` decorator in `worker.py`. - **Blueprints**: Each domain registers as a Quart Blueprint with its own `template_folder`. Templates in domain dirs override shared ones. - **Soft deletes**: `deleted_at` column pattern. Use `soft_delete()`, `restore()`, `hard_delete()` from `core.py`. - **Dates**: All stored as ISO 8601 TEXT in SQLite, using `datetime.utcnow().isoformat()`. - **Rate limiting**: SQLite-based, no Redis. `@rate_limit()` decorator or `check_rate_limit()` function. - **Migrations**: Single `schema.sql` file with all `CREATE TABLE IF NOT EXISTS`. Run via `python -m .migrations.migrate`. ### Conditional template blocks `billing/routes.py.jinja` conditionally generates the full billing implementation based on the `payment_provider` variable (stripe/paddle/lemonsqueezy). The `.env.example.jinja`, `core.py.jinja`, and `schema.sql.jinja` similarly adapt per provider. ## Design Philosophy Data-oriented, minimal abstraction. Plain SQL over ORM. Write code first, extract patterns only when repeated 3+ times. SQLite as the default database. Server-rendered HTML with Pico CSS. ## Ruff Config Line length 100, target Python 3.11+, rules: E, F, I, UP (ignoring E501).