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 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)
- A request arrives at Traefik, which routes
api.<domain>to the backend container. - FastAPI (
app/main.py) applies CORS middleware (origins fromsettings.all_cors_origins) and dispatches to the API router mounted atsettings.API_V1_STR(default/api/v1). - The router (
app/api/main.py) includes thelogin,users,utils, anditemsroute modules — plus aprivatemodule only whenENVIRONMENT == "local". - Protected endpoints resolve the
CurrentUserdependency (app/api/deps.py), which decodes the JWT, loads theUserfrom the database, and rejects inactive or unknown users. - Route handlers use a request-scoped SQLModel
Session(theSessionDepdependency) 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 fromimport.meta.env.VITE_API_URLand supplies the bearer token fromlocalStorage. - Auth interception: a shared query/mutation
onErrorhandler clears the token and redirects to/loginon any401/403, giving the SPA a single global auth-failure path. - Routing is file-based (TanStack Router). Routes under
_layoutrequire authentication — the_layoutroute'sbeforeLoadredirects to/loginwhen no token is present. The generated route tree lives inrouteTree.gen.ts. - Forms use
react-hook-formwithzodschemas 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-writtenfetch/axioscalls in feature code.
2.4 The generated-client contract
The frontend's src/client/ directory is produced by
@hey-api/openapi-ts 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 | Python dependency/venv manager (for running the backend locally) |
| Bun 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_KEYdefaults 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 fixedSECRET_KEYfor 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 |
(required) | Email of the auto-seeded first admin | |
FIRST_SUPERUSER_PASSWORD |
str | (required) | Password of the auto-seeded first admin |
EMAIL_TEST_USER |
[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
Userowns manyItems. Deleting a user cascade-deletes their items (cascade_delete=Trueon the relationship andondelete="CASCADE"on the FK). - Identifiers: primary keys are UUIDs (
default_factory=uuid.uuid4), not sequential integers — safer to expose and migration-friendly. - Timestamps:
created_atis timezone-aware UTC via aget_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
Itemfamily as a template: a*Base, atable=Truemodel, andCreate/Update/Public/*sPublicvariants. 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
privaterouter is mounted only whenENVIRONMENT == "local". It bypasses normal validation and must never be exposed in staging or production — keepENVIRONMENTcorrect 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'ssubclaim is the user's UUID;expis set fromACCESS_TOKEN_EXPIRE_MINUTES. - Transport: the OAuth2 password flow (
OAuth2PasswordBearer) at/login/access-token; clients sendAuthorization: Bearer <token>. - Resolution:
get_current_userdecodes the token, loads theUser, and rejects unknown (404) or inactive (400) users; invalid tokens yield403. - Authorization:
get_current_active_superusergates 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_updatetransparently 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) |
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_MINUTESand/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 theprivaterouter, enables Sentry, enforces secret checks). - [ ] Scope
BACKEND_CORS_ORIGINSto 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_DSNfor 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 incompose.override.yml(source mounts, exposed ports, mailcatcher) are not applied.
8.3 Common commands
# Start the full dev stack with live sync
docker compose watch
# Follow logs (all services, or one)
docker compose logs
docker compose logs backend
# Open a shell inside the running backend
docker compose exec backend 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.
# 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.pydocuments how to switch toSQLModel.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), sodashboard.,api.,adminer., andtraefik.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:
# Create the shared Docker network the stacks attach to
docker network create traefik-public
# 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
# 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:
rsync -av --filter=":- .gitignore" ./ root@your-server:/root/code/app/
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}"
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 tomaster.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) enforce these on every commit and also regenerate the
frontend client when backend files change and add release dates to release-notes.md.
# 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:
- Model — add
YourThingBase, atable=Truemodel, andCreate/Update/Publicvariants inbackend/app/models.py. - CRUD — add helpers in
backend/app/crud.pyif your logic exceeds simple session calls. - Routes — create
backend/app/api/routes/your_things.pywith anAPIRouter(prefix + tag), then include it inbackend/app/api/main.py. - Migration —
alembic revision --autogenerate -m "..."and commit it. - Client — run
bash ./scripts/generate-client.shto regenerate the typed client. - Frontend — add a route under
frontend/src/routes/_layout/and use the generatedYourThingsServicethrough TanStack Query hooks.
Tip
Give every route module a single, descriptive
tags=[...]entry —main.py'scustom_generate_unique_iduses 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_KEYis generated per process when unset, always pinSECRET_KEYbefore scaling the backend horizontally. - Items as a placeholder: the
Itemresource exists only as a worked example — remove it once you've copied the pattern for your real domain models.