# Full Stack FastAPI Template — Admin & Developer Guide

This guide is the architecture, configuration, security, deployment, and internals
reference for the **Full Stack FastAPI Template**. It is written for the people who
operate and extend a project built from this template: backend and frontend developers,
DevOps engineers, and architects.

The template is a complete, integrated three-tier web application — a FastAPI/SQLModel
backend, a React single-page frontend, and a PostgreSQL database — orchestrated by
Docker Compose and fronted by a Traefik reverse proxy that terminates TLS. It is
distributed as a [Copier](https://copier.readthedocs.io) project template: you generate
or clone it, then own and evolve the code.

---

## 1. Executive Summary

| Aspect | Detail |
|---|---|
| **Purpose** | Production-ready scaffold for a full-stack web app with auth, CRUD, and CI/CD |
| **Backend** | FastAPI, SQLModel (ORM), Pydantic, PostgreSQL, Alembic migrations |
| **Frontend** | React 19, TypeScript, Vite, TanStack Query/Router/Table, Tailwind CSS, shadcn/ui |
| **Auth** | JWT (HS256) bearer tokens; password hashing via Argon2 (with Bcrypt fallback) |
| **API client** | Auto-generated TypeScript client from the backend's OpenAPI schema |
| **Infra** | Docker Compose (dev + prod), Traefik proxy with automatic Let's Encrypt TLS |
| **Tooling** | uv (Python), Bun (JS), Ruff + Biome (lint/format), Pytest + Playwright (tests) |
| **CI/CD** | GitHub Actions — tests on PRs, deploy to staging on merge, production on release |
| **Distribution** | Copier template; MIT licensed |

The architecture's defining choice is a **contract-first, generated client**: the
backend's OpenAPI schema is the single source of truth, and the frontend's API layer is
generated from it. Backend and frontend stay in lockstep without hand-written glue.

---

## 2. Architecture

### 2.1 Component overview

```
                          Internet
                             │
                       ┌─────▼─────┐
                       │  Traefik  │  reverse proxy, TLS (Let's Encrypt)
                       │  (proxy)  │  routes by subdomain
                       └─────┬─────┘
            ┌────────────────┼────────────────┐
            │                │                │
   dashboard.<domain>   api.<domain>     adminer.<domain>
            │                │                │
      ┌─────▼─────┐    ┌─────▼─────┐    ┌─────▼─────┐
      │ frontend  │    │  backend  │    │  adminer  │
      │ nginx +   │    │ FastAPI   │    │ db web UI │
      │ React SPA │    │ (uvicorn) │    └─────┬─────┘
      └───────────┘    └─────┬─────┘          │
                             │                │
                       ┌─────▼────────────────▼─────┐
                       │      db (PostgreSQL 18)      │
                       │   volume: app-db-data        │
                       └──────────────────────────────┘

   prestart (one-shot): waits for db, runs Alembic migrations, seeds first superuser
   mailcatcher (dev only): captures outgoing email
   playwright (dev only): runs end-to-end tests
```

### 2.2 Request lifecycle (backend)

1. A request arrives at Traefik, which routes `api.<domain>` to the **backend** container.
2. FastAPI (`app/main.py`) applies CORS middleware (origins from
   `settings.all_cors_origins`) and dispatches to the API router mounted at
   `settings.API_V1_STR` (default `/api/v1`).
3. The router (`app/api/main.py`) includes the `login`, `users`, `utils`, and `items`
   route modules — plus a `private` module **only when `ENVIRONMENT == "local"`**.
4. Protected endpoints resolve the `CurrentUser` dependency (`app/api/deps.py`), which
   decodes the JWT, loads the `User` from the database, and rejects inactive or
   unknown users.
5. Route handlers use a request-scoped SQLModel `Session` (the `SessionDep` dependency)
   and return Pydantic/SQLModel response models, which FastAPI serializes to JSON.

### 2.3 Frontend architecture

- **Entry** (`frontend/src/main.tsx`) wires up TanStack Query (`QueryClient`), TanStack
  Router (`RouterProvider`), the theme provider, and a toast host. It configures the
  generated client's base URL from `import.meta.env.VITE_API_URL` and supplies the
  bearer token from `localStorage`.
- **Auth interception:** a shared query/mutation `onError` handler clears the token and
  redirects to `/login` on any `401`/`403`, giving the SPA a single global auth-failure path.
- **Routing** is file-based (TanStack Router). Routes under `_layout` require
  authentication — the `_layout` route's `beforeLoad` redirects to `/login` when no
  token is present. The generated route tree lives in `routeTree.gen.ts`.
- **Forms** use `react-hook-form` with `zod` schemas for client-side validation
  (e.g. the login and signup schemas mirror the backend's field constraints).
- **Data access** goes exclusively through the generated client services
  (`LoginService`, `UsersService`, `ItemsService`, …) wrapped in TanStack Query
  hooks — no hand-written `fetch`/`axios` calls in feature code.

### 2.4 The generated-client contract

The frontend's `src/client/` directory is produced by
[`@hey-api/openapi-ts`](https://heyapi.dev) from the backend's OpenAPI document.
The backend gives each operation a stable, predictable ID via
`custom_generate_unique_id()` in `app/main.py`, which names operations
`{tag}-{function_name}` (e.g. `items-read_items`). This keeps the generated method names
stable across regenerations. Regenerate with `scripts/generate-client.sh` whenever the
API changes; the pre-commit hooks also regenerate it when backend files change.

> [!IMPORTANT]
> Treat `frontend/src/client/` as **build output**, not source. Never edit it by hand —
> your changes are overwritten on the next generation. Change the backend, then regenerate.

---

## 3. Requirements

| Requirement | Notes |
|---|---|
| Docker Engine + Compose | The supported way to run the full stack (dev and prod) |
| [uv](https://docs.astral.sh/uv/) | Python dependency/venv manager (for running the backend locally) |
| [Bun](https://bun.sh/) or Node.js | Frontend package manager / dev server (Bun recommended) |
| PostgreSQL 18 | Provided by the `db` Compose service; or an external managed instance |
| A domain + DNS (production) | Wildcard subdomain pointing at your server (see Deployment) |

---

## 4. Configuration Reference

All runtime configuration is environment-driven. The authoritative schema is the
`Settings` class in `backend/app/core/config.py` (a Pydantic `BaseSettings`), which reads
from the project-root `.env` file. Values marked *computed* are derived, not set directly.

### 4.1 Core application settings

| Variable | Type | Default | Description |
|---|---|---|---|
| `PROJECT_NAME` | str | *(required)* | Display name in API docs and emails |
| `API_V1_STR` | str | `/api/v1` | Base path prefix for all API routes |
| `ENVIRONMENT` | enum | `local` | One of `local`, `staging`, `production` |
| `DOMAIN` | str | `localhost` | Base domain; used by Traefik routing and TLS |
| `FRONTEND_HOST` | str | `http://localhost:5173` | Frontend base URL; used to build email links |
| `STACK_NAME` | str | *(from .env)* | Docker Compose project/label name (no spaces or dots) |
| `SECRET_KEY` | str | random per-process | Signs JWTs; **must be set** in deployments |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | int | `11520` (8 days) | Access-token lifetime |
| `BACKEND_CORS_ORIGINS` | list/csv | `[]` | Allowed browser origins (comma-separated or JSON list) |
| `all_cors_origins` | *computed* | — | `BACKEND_CORS_ORIGINS` (trailing slash stripped) plus `FRONTEND_HOST` |
| `SENTRY_DSN` | URL/None | `None` | Enables Sentry when set and `ENVIRONMENT != "local"` |

> [!NOTE]
> `SECRET_KEY` defaults to a fresh random value generated **at process start**
> (`secrets.token_urlsafe(32)`). That is convenient locally but means tokens are
> invalidated on every restart and differ across replicas — so you **must** set a fixed
> `SECRET_KEY` for any real deployment.

### 4.2 Database settings

| Variable | Type | Default | Description |
|---|---|---|---|
| `POSTGRES_SERVER` | str | *(required)* | Database hostname (`db` inside Compose) |
| `POSTGRES_PORT` | int | `5432` | Database port |
| `POSTGRES_USER` | str | *(required)* | Database user |
| `POSTGRES_PASSWORD` | str | `""` | Database password |
| `POSTGRES_DB` | str | `""` | Database name |
| `SQLALCHEMY_DATABASE_URI` | *computed* | — | Built as `postgresql+psycopg://…` from the above |

The connection string is assembled with Pydantic's `PostgresDsn.build()` using the
`psycopg` (v3) driver — you never set the URI directly.

### 4.3 Email (SMTP) settings

| Variable | Type | Default | Description |
|---|---|---|---|
| `SMTP_HOST` | str/None | `None` | SMTP server host; required to enable email |
| `SMTP_PORT` | int | `587` | SMTP port |
| `SMTP_USER` | str/None | `None` | SMTP username |
| `SMTP_PASSWORD` | str/None | `None` | SMTP password |
| `SMTP_TLS` | bool | `True` | Use STARTTLS |
| `SMTP_SSL` | bool | `False` | Use implicit SSL |
| `EMAILS_FROM_EMAIL` | email/None | `None` | "From" address |
| `EMAILS_FROM_NAME` | str/None | `PROJECT_NAME` | "From" display name (defaults to project name) |
| `EMAIL_RESET_TOKEN_EXPIRE_HOURS` | int | `48` | Lifetime of password-reset tokens |
| `emails_enabled` | *computed* | — | `True` only when both `SMTP_HOST` and `EMAILS_FROM_EMAIL` are set |

### 4.4 Bootstrap & operational settings

| Variable | Type | Default | Description |
|---|---|---|---|
| `FIRST_SUPERUSER` | email | *(required)* | Email of the auto-seeded first admin |
| `FIRST_SUPERUSER_PASSWORD` | str | *(required)* | Password of the auto-seeded first admin |
| `EMAIL_TEST_USER` | email | `[email removed]` | Address used by the test-email utility |

The following are consumed by the Docker Compose files (not the `Settings` class):

| Variable | Default | Description |
|---|---|---|
| `DOCKER_IMAGE_BACKEND` | `backend` | Backend image name for your registry |
| `DOCKER_IMAGE_FRONTEND` | `frontend` | Frontend image name for your registry |
| `TAG` | `latest` | Image tag to deploy |
| `VITE_API_URL` | per-build | Frontend build arg: the API base URL baked into the SPA |

### 4.5 The "changethis" guard

Two `model_validator(mode="after")` hooks in `Settings` enforce secret hygiene at
startup. `_enforce_non_default_secrets()` checks `SECRET_KEY`, `POSTGRES_PASSWORD`, and
`FIRST_SUPERUSER_PASSWORD`: if any equals `changethis`, it **warns** when
`ENVIRONMENT == "local"` and **raises a `ValueError`** otherwise — so a misconfigured
staging/production backend fails fast instead of running with a known-public secret.

---

## 5. Data Model

All models live in `backend/app/models.py` as SQLModel classes, which serve double duty
as database tables and API schemas. The pattern is a `*Base` of shared fields, table
models (`table=True`), and create/update/public variants.

### 5.1 Entities

```
User (table)                          Item (table)
├── id: UUID (pk)                      ├── id: UUID (pk)
├── email: str (unique, indexed)      ├── title: str (1–255)
├── is_active: bool = True            ├── description: str | None (≤255)
├── is_superuser: bool = False        ├── created_at: datetime (UTC)
├── full_name: str | None             ├── owner_id: UUID → user.id (CASCADE)
├── hashed_password: str              └── owner: User
├── created_at: datetime (UTC)
└── items: list[Item] (cascade delete)
```

- **Relationship:** one `User` owns many `Item`s. Deleting a user cascade-deletes their
  items (`cascade_delete=True` on the relationship and `ondelete="CASCADE"` on the FK).
- **Identifiers:** primary keys are UUIDs (`default_factory=uuid.uuid4`), not sequential
  integers — safer to expose and migration-friendly.
- **Timestamps:** `created_at` is timezone-aware UTC via a `get_datetime_utc()` factory.
- **Validation:** passwords are constrained to 8–128 chars; emails use Pydantic `EmailStr`.

### 5.2 Schema variants

| Class | Role |
|---|---|
| `UserCreate` / `UserRegister` | Admin-created vs. self-service signup payloads |
| `UserUpdate` / `UserUpdateMe` | Full (admin) vs. self-service profile update |
| `UpdatePassword` / `NewPassword` | Change-password vs. reset-via-token payloads |
| `UserPublic` / `UsersPublic` | Single user vs. paginated list (`data` + `count`) responses |
| `ItemCreate` / `ItemUpdate` / `ItemPublic` / `ItemsPublic` | Item CRUD + list schemas |
| `Token` / `TokenPayload` | JWT response and decoded-claims models |
| `Message` | Generic `{ "message": str }` response |

> [!TIP]
> When you add your own resource, copy the `Item` family as a template: a `*Base`,
> a `table=True` model, and `Create`/`Update`/`Public`/`*sPublic` variants. This keeps
> request validation, DB schema, and API responses consistent and OpenAPI-friendly.

---

## 6. API Reference

All paths are relative to `API_V1_STR` (default `/api/v1`). "Auth" indicates the
requirement: **Public** (none), **User** (valid token), or **Superuser**.

### 6.1 Authentication & password recovery (`login`)

| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | `/login/access-token` | Public | OAuth2 password flow; returns an access token |
| POST | `/login/test-token` | User | Validate the current token, return the user |
| POST | `/password-recovery/{email}` | Public | Send a password-recovery email |
| POST | `/reset-password/` | Public | Reset a password using an emailed token |
| POST | `/password-recovery-html-content/{email}` | Superuser | Preview recovery email HTML |

### 6.2 Users (`users`)

| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | `/users/` | Superuser | List users (paginated) |
| POST | `/users/` | Superuser | Create a user (sends new-account email if enabled) |
| GET | `/users/me` | User | Get the current user |
| PATCH | `/users/me` | User | Update own profile |
| PATCH | `/users/me/password` | User | Change own password |
| DELETE | `/users/me` | User | Delete own account (superusers forbidden) |
| POST | `/users/signup` | Public | Self-service registration |
| GET | `/users/{user_id}` | User | Get a user by ID (own, or any if superuser) |
| PATCH | `/users/{user_id}` | Superuser | Update any user |
| DELETE | `/users/{user_id}` | Superuser | Delete any user (cannot delete self) |

### 6.3 Items (`items`)

| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | `/items/` | User | List items (own; all if superuser), paginated |
| POST | `/items/` | User | Create an item owned by the caller |
| GET | `/items/{id}` | User | Get an item (owner or superuser) |
| PUT | `/items/{id}` | User | Update an item (owner or superuser) |
| DELETE | `/items/{id}` | User | Delete an item (owner or superuser) |

### 6.4 Utilities (`utils`) and private (`private`)

| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | `/utils/test-email/` | Superuser | Send a test email |
| GET | `/utils/health-check/` | Public | Liveness check, returns `true` |
| POST | `/private/users/` | *local only* | Create a user without restrictions (testing) |

> [!CAUTION]
> The `private` router is mounted **only when `ENVIRONMENT == "local"`**. It bypasses
> normal validation and must never be exposed in staging or production — keep
> `ENVIRONMENT` correct on every deployment.

Pagination follows a consistent pattern: list endpoints accept `skip` (default `0`) and
`limit` (default `100`) query parameters and return `{ "data": [...], "count": <total> }`.

---

## 7. Security

### 7.1 Authentication

- **Tokens:** JWT signed with **HS256** using `SECRET_KEY` (`app/core/security.py`).
  The token's `sub` claim is the user's UUID; `exp` is set from
  `ACCESS_TOKEN_EXPIRE_MINUTES`.
- **Transport:** the OAuth2 password flow (`OAuth2PasswordBearer`) at
  `/login/access-token`; clients send `Authorization: Bearer <token>`.
- **Resolution:** `get_current_user` decodes the token, loads the `User`, and rejects
  unknown (`404`) or inactive (`400`) users; invalid tokens yield `403`.
- **Authorization:** `get_current_active_superuser` gates admin-only routes; per-object
  checks (e.g. item ownership) happen inside handlers.

### 7.2 Password handling

- Hashing uses **pwdlib** with an Argon2 primary hasher and a Bcrypt fallback
  (`PasswordHash((Argon2Hasher(), BcryptHasher()))`).
- `verify_and_update` transparently **re-hashes** a password on successful login if it
  was stored with an older scheme — so migrating hash algorithms is automatic over time.
- Password length is bounded (8–128) at the schema layer to keep hashing predictable.

### 7.3 Attack-surface mitigations (built in)

| Concern | Mitigation in the code |
|---|---|
| **Timing attacks on login** | `authenticate()` verifies against a constant `DUMMY_HASH` even when the email doesn't exist, equalizing response time |
| **Email enumeration (recovery)** | `/password-recovery/{email}` always returns the same "if that email is registered…" message, sending mail only if the user exists |
| **Email enumeration (reset)** | A non-existent user on `/reset-password/` returns the same `Invalid token` error as a bad token |
| **Self-lockout** | Superusers cannot delete themselves (`403`) |
| **CORS** | Only origins in `all_cors_origins` may call the API from a browser |
| **Secret hygiene** | Startup refuses `changethis` secrets outside `local` (see [4.5](#45-the-changethis-guard)) |

> [!WARNING]
> Tokens are long-lived (8 days by default) and there is no server-side revocation
> list. If you need immediate revocation (e.g. on compromise), shorten
> `ACCESS_TOKEN_EXPIRE_MINUTES` and/or add a token-version claim and check it against the
> user record.

### 7.4 Hardening checklist for production

- [ ] Set strong, fixed `SECRET_KEY`, `POSTGRES_PASSWORD`, `FIRST_SUPERUSER_PASSWORD`.
- [ ] Set `ENVIRONMENT=production` (disables the `private` router, enables Sentry, enforces secret checks).
- [ ] Scope `BACKEND_CORS_ORIGINS` to your real frontend origins only.
- [ ] Rotate the first superuser password after bootstrap, or disable the account.
- [ ] Terminate TLS at Traefik and never expose the backend/db ports publicly.
- [ ] Configure `SENTRY_DSN` for error visibility.

---

## 8. Operations

### 8.1 Service inventory (Docker Compose)

| Service | Image / build | Role | Exposed (dev) |
|---|---|---|---|
| `db` | `postgres:18` | Database; health-checked with `pg_isready` | `5432` |
| `prestart` | backend image | One-shot: waits for DB, runs migrations, seeds superuser | — |
| `backend` | `backend/Dockerfile` | FastAPI API (uvicorn) | `8000` |
| `frontend` | `frontend/Dockerfile` | nginx serving the built React SPA | `5173` |
| `adminer` | `adminer` | Web database admin UI | `8080` |
| `proxy` (override) | `traefik:3.6` | Local reverse proxy + dashboard | `80`, `8090` |
| `mailcatcher` (override) | `schickling/mailcatcher` | Captures dev email | `1080`, `1025` |
| `playwright` (override) | `frontend/Dockerfile.playwright` | E2E test runner | `9323` |

The `prestart` service runs `scripts/prestart.sh` and must complete successfully
(`service_completed_successfully`) before `backend` starts — this is how migrations and
seeding happen deterministically on every deploy.

### 8.2 Compose file layout

| File | Used when | Purpose |
|---|---|---|
| `compose.yml` | always | Base stack: db, prestart, backend, frontend, adminer |
| `compose.override.yml` | local dev (automatic) | Adds proxy, mailcatcher, playwright; mounts source; enables `--reload` and port exposure |
| `compose.traefik.yml` | production, once | The shared public Traefik proxy (run separately on the server) |

> [!IMPORTANT]
> In production, run with `docker compose -f compose.yml ...` **explicitly** so the
> development overrides in `compose.override.yml` (source mounts, exposed ports,
> mailcatcher) are **not** applied.

### 8.3 Common commands

```bash
# Start the full dev stack with live sync
docker compose watch
```

```bash
# Follow logs (all services, or one)
docker compose logs
docker compose logs backend
```

```bash
# Open a shell inside the running backend
docker compose exec backend bash
```

```bash
# Run the backend test suite against the running stack
docker compose exec backend bash scripts/tests-start.sh
```

### 8.4 Database migrations (Alembic)

Schema changes are managed with Alembic; migrations live in
`backend/app/alembic/versions/` and are applied automatically by `prestart` on startup.

```bash
# Inside the backend container, after editing app/models.py
alembic revision --autogenerate -m "Add column last_name to User"
alembic upgrade head
```

Commit the generated revision file to your repository. Alembic is pre-wired to import
the SQLModel metadata from `app/models.py`.

> [!NOTE]
> If you prefer not to use migrations at all, `app/core/db.py` documents how to switch
> to `SQLModel.metadata.create_all(engine)` instead — but migrations are strongly
> recommended for any project that will evolve.

### 8.5 Startup resilience

The `backend_pre_start.py` and `tests_pre_start.py` scripts use **tenacity** to retry a
trivial `SELECT 1` for up to 5 minutes (one attempt per second) until the database
answers. This is why the first boot can take a minute — the backend is waiting for
PostgreSQL's health check, not hung.

---

## 9. Deployment

The template deploys with Docker Compose to a remote server, behind a single public
Traefik proxy that handles TLS for every stack on the host.

### 9.1 Preparation

- A remote server with **Docker Engine** (not Docker Desktop) installed.
- DNS configured with a **wildcard subdomain** (e.g. `*.fastapi-project.example.com`),
  so `dashboard.`, `api.`, `adminer.`, and `traefik.` subdomains all resolve to the server.

### 9.2 One-time: the public Traefik proxy

Traefik runs once per server and is shared by all stacks. On the server:

```bash
# Create the shared Docker network the stacks attach to
docker network create traefik-public
```

```bash
# Provide Traefik's required env vars (dashboard basic-auth + ACME email)
export USERNAME=admin
export PASSWORD=<a-strong-password>
export HASHED_PASSWORD=$(openssl passwd -apr1 $PASSWORD)
export DOMAIN=fastapi-project.example.com
export EMAIL=[email removed]    # must be a real address for Let's Encrypt
```

```bash
# Start Traefik
docker compose -f compose.traefik.yml up -d
```

Traefik obtains and renews Let's Encrypt certificates automatically via the TLS
challenge, storing them in the `traefik-public-certificates` volume.

### 9.3 Deploy the application stack

Copy the code to the server (respecting `.gitignore`), set the required environment
variables, then build and start:

```bash
rsync -av --filter=":- .gitignore" ./ root@your-server:/root/code/app/
```

```bash
export ENVIRONMENT=production
export DOMAIN=fastapi-project.example.com
export SECRET_KEY=<generated>
export POSTGRES_PASSWORD=<generated>
export FIRST_SUPERUSER_PASSWORD=<generated>
export BACKEND_CORS_ORIGINS="https://dashboard.${DOMAIN},https://api.${DOMAIN}"
```

```bash
cd /root/code/app/
docker compose -f compose.yml build
docker compose -f compose.yml up -d
```

Traefik routes traffic to each service by subdomain via the labels in `compose.yml`:
`api.<domain>` → backend (port 8000), `dashboard.<domain>` → frontend (port 80),
`adminer.<domain>` → adminer.

### 9.4 Continuous Deployment (GitHub Actions)

The repository ships workflows for automated deployment to two GitHub Environments:

- **`staging`** — deploys after pushing/merging to `master`.
- **`production`** — deploys after publishing a release.

Both run on **self-hosted GitHub Actions runners** labeled `staging` / `production` that
you install on your server(s) as a service. Configure these per-environment secrets:

| Secret | Used for |
|---|---|
| `DOMAIN_PRODUCTION` / `DOMAIN_STAGING` | Target domains |
| `STACK_NAME_PRODUCTION` / `STACK_NAME_STAGING` | Compose stack/label names |
| `SECRET_KEY` | JWT signing |
| `POSTGRES_PASSWORD` | Database password |
| `FIRST_SUPERUSER` / `FIRST_SUPERUSER_PASSWORD` | Seed admin |
| `EMAILS_FROM_EMAIL` | Outgoing mail sender |
| `LATEST_CHANGES` | Auto release-note generation (PAT) |
| `SMOKESHOW_AUTH_KEY` | Publishing coverage reports |

> [!TIP]
> Use GitHub **environment** secrets (not repository secrets) so they are scoped to
> staging vs. production and can carry deployment protection rules (required reviewers,
> wait timers).

---

## 10. Testing & Quality

| Layer | Tool | How to run |
|---|---|---|
| Backend unit/integration | **Pytest** | `docker compose exec backend bash scripts/tests-start.sh` |
| Backend coverage | coverage.py | Generates `backend/htmlcov/index.html` |
| Frontend end-to-end | **Playwright** | `cd frontend && bunx playwright test` (stack running) |
| Python lint/format | **Ruff** | via pre-commit / `uv run ruff` |
| Python type-check | **mypy** and **ty** | via pre-commit |
| JS/TS lint/format | **Biome** | `bun run lint` |

Pre-commit hooks (configured in `.pre-commit-config.yaml`, run via
[prek](https://prek.j178.dev/)) enforce these on every commit and also **regenerate the
frontend client** when backend files change and add release dates to `release-notes.md`.

```bash
# Install the git hook (from the backend folder)
uv run prek install -f

# Run all hooks manually across the repo
uv run prek run --all-files
```

> [!NOTE]
> The pre-commit pipeline runs Ruff, ruff-format, Biome, mypy, ty, and the SDK
> generator. Run it locally before pushing to avoid CI failures and stale clients.

---

## 11. Repository Structure

```
.
├── backend/                  FastAPI backend (Python, uv)
│   ├── app/
│   │   ├── main.py           FastAPI app + CORS + router mount
│   │   ├── models.py         SQLModel models = tables + API schemas
│   │   ├── crud.py           DB create/read/update helpers
│   │   ├── utils.py          Email rendering/sending, reset-token helpers
│   │   ├── api/
│   │   │   ├── main.py       API router aggregation
│   │   │   ├── deps.py       Auth/session dependencies
│   │   │   └── routes/       login, users, items, utils, private
│   │   ├── core/
│   │   │   ├── config.py     Settings (env schema) — config source of truth
│   │   │   ├── db.py         Engine + initial-data seeding
│   │   │   └── security.py   JWT + password hashing
│   │   ├── alembic/          Migrations
│   │   └── email-templates/  MJML sources + built HTML
│   ├── Dockerfile
│   └── pyproject.toml
├── frontend/                 React SPA (TypeScript, Bun, Vite)
│   ├── src/
│   │   ├── main.tsx          App bootstrap (Query + Router + theme)
│   │   ├── client/           GENERATED OpenAPI client (do not edit)
│   │   ├── routes/           File-based pages (login, signup, _layout/*)
│   │   ├── components/       UI + feature components (shadcn/ui based)
│   │   └── hooks/            useAuth and helpers
│   ├── Dockerfile
│   └── package.json
├── compose.yml               Base stack
├── compose.override.yml      Dev overrides
├── compose.traefik.yml       Public proxy (prod)
├── copier.yml                Template input variables
├── .env                      Configuration values
└── scripts/                  generate-client, test, prestart helpers
```

---

## 12. Extending the Template — Adding a Resource

The cleanest path to your own feature mirrors how `Item` is built:

1. **Model** — add `YourThingBase`, a `table=True` model, and `Create`/`Update`/`Public`
   variants in `backend/app/models.py`.
2. **CRUD** — add helpers in `backend/app/crud.py` if your logic exceeds simple
   session calls.
3. **Routes** — create `backend/app/api/routes/your_things.py` with an `APIRouter`
   (prefix + tag), then include it in `backend/app/api/main.py`.
4. **Migration** — `alembic revision --autogenerate -m "..."` and commit it.
5. **Client** — run `bash ./scripts/generate-client.sh` to regenerate the typed client.
6. **Frontend** — add a route under `frontend/src/routes/_layout/` and use the generated
   `YourThingsService` through TanStack Query hooks.

> [!TIP]
> Give every route module a single, descriptive `tags=[...]` entry — `main.py`'s
> `custom_generate_unique_id` uses the first tag to name generated client methods, so
> consistent tags produce clean, stable frontend service names.

---

## 13. Potential Improvements & Considerations

These are observations for teams adopting the template, not defects — the template is
intentionally minimal so you can shape it.

- **Token revocation:** there is no refresh-token or revocation mechanism. For
  security-sensitive apps, consider shorter access-token lifetimes plus a refresh flow,
  or a token-version claim checked against the user.
- **Rate limiting:** the API has no built-in rate limiting on login or recovery
  endpoints. Add one (e.g. at Traefik or via middleware) to blunt brute-force attempts.
- **`deploy.sh`/secrets in env:** production secrets are passed as shell environment
  variables in the documented flow; integrate a secrets manager for stronger handling.
- **Single backend replica:** because a random `SECRET_KEY` is generated per process when
  unset, always pin `SECRET_KEY` before scaling the backend horizontally.
- **Items as a placeholder:** the `Item` resource exists only as a worked example —
  remove it once you've copied the pattern for your real domain models.

---

_Generated June 2026 from fastapi/full-stack-fastapi-template at 38302d7. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
