# FastAPI Skeleton A production-shaped starting point for a web app that serves both **HTML (HTMX)** and a **JSON API**. ## Stack | Concern | Choice | |----------------|--------| | Web framework | FastAPI | | HTML / interactivity | Jinja2 + HTMX | | ORM | SQLAlchemy 2.0 (async) | | Migrations | Alembic | | Database | SQLite (default) or Postgres — one `DATABASE_URL` | | Auth | JWT access + refresh tokens, rotation, DB-backed revocation | | Authorization | RBAC: users → roles → permissions | | Admin UI | Custom HTMX dashboard (user & role CRUD) | | Config | env vars **or** `.env` (pydantic-settings) | | Tooling | uv, pytest, ruff | | Packaging | Dockerfile + docker-compose | ## Layout ``` app/ core/ config, database engine/session, security (hashing + JWT) models/ SQLAlchemy models: user, role, permission, refresh_token schemas/ Pydantic request/response models crud/ data-access helpers services/ auth token issuance / rotation deps.py DI: get_db, current user, require_permission(...) api/ JSON API: auth, users, roles web/ HTML/HTMX routes: pages + admin dashboard templates/ Jinja2 templates (+ admin partials) static/ CSS alembic/ migration environment + initial migration scripts/seed.py bootstrap permissions, roles, first superuser tests/ pytest (auth + RBAC) ``` ## Starting a new project Fork or copy this repo, then update these six things before writing any code: ### 1. Project package name — `pyproject.toml` ```toml [project] name = "my-app" # used by uv/pip description = "What your app does." ``` ### 2. Application display name — `.env` / environment The name shown in the browser title bar, nav bar, and `/docs` page title is driven by `APP_NAME` in your `.env` (or any environment variable override): ```bash APP_NAME="My App" ``` This is the only runtime change needed — templates read `{{ app_name }}` from `settings.app_name`, which is sourced from this variable. No template edits required. ### 3. Superuser credentials — `.env` ```bash FIRST_SUPERUSER_EMAIL=admin@yourapp.com FIRST_SUPERUSER_PASSWORD= ``` ### 4. Secret key — `.env` ```bash SECRET_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')" ``` Never commit a real secret key. The placeholder in `.env.example` is intentionally weak. ### 5. Docker / Postgres identifiers — `docker-compose.yml` The compose file uses `app` as the Postgres username, password, and database name. Rename them consistently: ```yaml # db service POSTGRES_USER: myapp POSTGRES_PASSWORD: POSTGRES_DB: myapp # app service DATABASE_URL: postgresql+asyncpg://myapp:@db:5432/myapp ``` ### 6. This README Replace the title and description with your project's own. --- After these changes, run the normal setup: ```bash uv sync --extra dev cp .env.example .env # edit the values above uv run python -m scripts.seed uv run uvicorn app.main:app --reload ``` ## Quick start (local, SQLite) ```bash # 1. Install dependencies (https://docs.astral.sh/uv/) uv sync --extra dev # 2. Configure (optional — sensible defaults exist) cp .env.example .env # then edit SECRET_KEY etc. # 3. Seed default roles/permissions + the first superuser uv run python -m scripts.seed # 4. Run uv run uvicorn app.main:app --reload ``` Open: - http://localhost:8000/ — HTMX dashboard (log in with the seeded superuser) - http://localhost:8000/docs — interactive API docs - http://localhost:8000/admin/users — admin dashboard Default superuser (override via env/`.env`): `admin@example.com` / `changethis`. ## Run with Docker (Postgres) ```bash docker compose up --build ``` This starts Postgres, seeds the database, and serves the app on http://localhost:8000. The app connects with `DATABASE_URL=postgresql+asyncpg://app:app@db:5432/app`. ## Configuration Every setting is read from an environment variable first, then `.env` (see `.env.example`). Key ones: | Variable | Default | Notes | |----------|---------|-------| | `SECRET_KEY` | dev placeholder | **set a strong value in production** | | `DATABASE_URL` | `sqlite+aiosqlite:///./app.db` | use `postgresql+asyncpg://...` for Postgres | | `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | | | `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | | | `AUTO_CREATE_TABLES` | `true` | create tables on startup (dev); use Alembic in prod | | `COOKIE_SECURE` | `false` | set `true` behind HTTPS | | `FIRST_SUPERUSER_EMAIL` / `_PASSWORD` | admin@example.com / changethis | used by seed | ## Auth model - `POST /api/auth/register` — self-service signup (gets the default `user` role) - `POST /api/auth/login` — OAuth2 password form (`username` = email) → access + refresh - `POST /api/auth/refresh` — rotates: the presented refresh token is revoked, a new pair is issued - `POST /api/auth/logout` — revoke one refresh token (body) or all for the user The JSON API authenticates via `Authorization: Bearer `. The HTMX UI stores the access token in an httpOnly cookie so pages work without client JS. Refresh tokens are stored server-side as a SHA-256 hash + `jti` with a `revoked` flag, so logout and rotation genuinely invalidate them. ## RBAC `users → roles → permissions`. Permissions follow `resource:action` (`users:read`, `users:write`, `users:delete`, `roles:read`, `roles:write`). Protect an endpoint with the dependency factory: ```python @router.get("", dependencies=[Depends(require_permission("users:read"))]) async def list_users(...): ... ``` Superusers (`is_superuser=True`) bypass all permission checks. ## Migrations (Alembic) `AUTO_CREATE_TABLES` is convenient for dev/SQLite. For real deployments use migrations instead: ```bash # apply the bundled initial migration uv run alembic upgrade head # after changing models, autogenerate the next revision uv run alembic revision --autogenerate -m "describe change" uv run alembic upgrade head ``` The Alembic env reads `DATABASE_URL` from app settings, so it targets the same database as the app. ## Tests ```bash uv run pytest ``` Tests run against an in-memory SQLite database and cover the auth lifecycle (register/login/me/refresh/logout) and RBAC enforcement. ## Where to go next - Add your own models in `app/models/`, schemas in `app/schemas/`, routes in `app/api/` or `app/web/`. - Add permissions to `scripts/seed.py` and guard routes with `require_permission(...)`. - Swap `HS256` for asymmetric `RS256` keys if you need to verify tokens in other services.