An opinionated FastAPI backend starter: JWT authentication, PostgreSQL + Alembic, and Celery/Redis for background email. Ships with Docker Compose for local development and a pytest test suite.
- JWT bearer auth — log in with either username or email; passwords hashed with bcrypt.
- Registration —
POST /auth/registercreates a user (role:adminoruser). - Password reset — DB-backed, single-use reset tokens (SHA-256 hashed) with expiry; the reset email is dispatched to Celery so the request returns immediately.
- Current-user endpoints — read profile, update email/username, change password, delete account.
- SQLAlchemy 2.0 declarative models with
TimestampMixin(created_at/updated_at). - Alembic migrations.
- Celery worker (Redis broker) for outbound email, with Flower for monitoring.
- HTML email templates rendered with Jinja2.
- Docker Compose stack: app, Postgres, Redis, Redis Commander, Celery worker, Flower.
- pytest suite running against in-memory-ish SQLite.
| Area | Choice |
|---|---|
| Web framework | FastAPI (fastapi[standard]) |
| ORM | SQLAlchemy 2.0 + psycopg2-binary |
| Migrations | Alembic |
| Config | pydantic-settings (.env) |
| Auth | python-jose (JWT) + passlib[bcrypt] |
| Task queue | Celery + Redis |
| Task monitoring | Flower |
emails (SMTP) + Jinja2 templates |
|
| Tooling | uv, Docker, pytest, ruff, mypy |
.
├── app/
│ ├── main.py # FastAPI app + router registration
│ ├── tasks.py # Celery app and tasks
│ ├── utils.py # Email rendering + sending helpers
│ ├── core/
│ │ ├── settings.py # Pydantic settings (env-driven)
│ │ ├── db.py # SQLAlchemy engines/sessions
│ │ ├── deps.py # db/user dependencies, current-user parsing
│ │ └── security.py # JWT, password hashing, reset tokens
│ ├── domain/ # Feature modules (models, schemas, actions)
│ │ ├── auth/
│ │ │ ├── actions/ # login, register, forgot/reset password
│ │ │ ├── helpers/ # authenticate_user
│ │ │ ├── models.py # PasswordResetToken
│ │ │ └── schemas.py
│ │ └── user/
│ │ ├── actions/ # change password, update profile
│ │ ├── enums.py # UserRole
│ │ ├── models.py # User
│ │ └── schemas.py
│ ├── models/
│ │ ├── __init__.py # Base + model exports
│ │ ├── base.py
│ │ └── mixins.py # TimestampMixin
│ ├── routers/
│ │ ├── auth.py
│ │ └── users.py
│ └── email_templates/
│ ├── src/ # Template sources (MJML)
│ └── build/ # Compiled HTML (generated; gitignored)
├── alembic/ # Migration environment + versions
├── scripts/ # format, lint, migrate, set-up-dev, test...
├── test/ # pytest suite
├── .docker/ # Dockerfile, entrypoints, compose files, nginx
├── pyproject.toml
└── docker-compose.yml # generated by scripts/set-up-dev.sh
- Python >= 3.12
- uv for dependency management
- Docker + Docker Compose (for Postgres, Redis, Celery worker, Flower)
- Something to compile email templates (
app/email_templates/src/*.mjml→build/*.html)
The quickest path is the setup script, which syncs deps, writes .env, brings up
Docker Compose, and runs migrations:
cp .docker/docker-compose.dev.yml docker-compose.yml # or let the script do it
chmod +x ./scripts/set-up-dev.sh
./scripts/set-up-dev.shOr step by step:
uv sync
cp example.dev.env .env
cp .docker/docker-compose.dev.yml docker-compose.yml
docker compose up -d --build
chmod +x ./scripts/migrate.sh && ./scripts/migrate.shThe API is served by the app container via nginx; see the compose file for ports.
Settings load from .env (see example.dev.env / example.prod.env) via
app/core/settings.py. Unknown keys are ignored.
| Variable | Default | Notes |
|---|---|---|
ENV |
local |
local | staging | production; controls which DB host/port is used |
PROJECT_NAME |
— | App title, used in emails |
SECRET_KEY |
random | Set this. Signs JWTs |
ALGORITHM |
HS256 |
JWT algorithm |
ACCESS_TOKEN_EXPIRE_MINUTES |
11520 |
8 days |
FRONTEND_HOST |
http://localhost:3000 |
Included in CORS + reset links |
DATABASE_NAME / DATABASE_USER / DATABASE_PASSWORD |
— | Postgres credentials |
DATABASE_HOST / DATABASE_PORT |
— / 6543 |
Used when ENV=local (host machine) |
DATABASE_DOCKER_NETWORK_HOST / DATABASE_DOCKER_NETWORK_PORT |
— / 5432 |
Used when ENV!=local (inside Docker) |
BACKEND_CORS_ORIGINS |
[] |
Comma-separated list |
CELERY_BROKER_URI |
redis://localhost:6379 |
Redis URL for Celery |
SMTP_HOST / SMTP_PORT |
— / 587 |
SMTP server |
SMTP_TLS / SMTP_SSL |
true / false |
Transport security |
SMTP_USER / SMTP_PASSWORD |
— | SMTP credentials |
EMAILS_FROM_EMAIL / EMAILS_FROM_NAME |
— / PROJECT_NAME |
Sender identity |
EMAIL_RESET_TOKEN_EXPIRE_MINUTES |
60 |
Reset-token lifetime |
Email sending is gated by settings.emails_enabled (SMTP_HOST and
EMAILS_FROM_EMAIL must both be set); otherwise send_email asserts.
Base path: /api/v1. Interactive docs at /docs (ReDoc at /redoc);
OpenAPI at /api/v1/openapi.json.
| Method | Path | Body | Auth | Description |
|---|---|---|---|---|
POST |
/login/access_token |
form: username, password |
— | Log in (username or email) → bearer token |
POST |
/register |
CreateUserRequest |
— | Create a user → UserResponse (201) |
POST |
/forgot-password |
{ "email": ... } |
— | Queue a reset email (202) |
POST |
/reset-password |
{ "token": ..., "password": ... } |
— | Set a new password |
All require a bearer token.
| Method | Path | Body | Description |
|---|---|---|---|
GET |
/ |
— | Current user profile |
PATCH |
/ |
UpdateUserRequest (email, username) |
Update profile (204) |
PUT |
/change_password |
ChangeUserPasswordRequest |
Change password (204) |
DELETE |
/ |
— | Delete current user (204) |
Templates are authored as MJML under app/email_templates/src/ and compiled to
HTML in app/email_templates/build/. At runtime app/utils.py reads the built
HTML and renders it with Jinja2 (render_email_template), then sends it over
SMTP via the emails package.
src/*.mjml --(build)--> build/*.html --(Jinja2 + context)--> send_email() -> SMTP
app/email_templates/build/ is gitignored and must be generated. Email builders
in app/utils.py: generate_password_reset_email, generate_new_account_email,
generate_test_email. Only the reset email is wired up (app/tasks.py →
Celery task send_password_reset_email).
Planned migration: replace the MJML sources with React Email components in a
packages/react-emailbun workspace, exported toapp/email_templates/build/at build time. Jinja2 stays as the runtime placeholder substituter. Not yet applied.
Alembic config lives in alembic.ini and alembic/. Create and apply a migration:
./scripts/migrate.sh "add some column" # autogenerate revision + upgrade head
app/main.pycallsBase.metadata.create_all(engine)at import time, so tables are also created automatically when the app starts. Treat Alembic as the source of truth for schema changes.
Tests live in test/ and use a SQLite database with dependency overrides:
bash scripts/test.sh # coverage run -m pytest tests/ (+ HTML report)
# or
uv run pytest test/
scripts/test.shinvokespytest tests/(with ans), while the suite in this repo lives intest/— adjust the path if the runner can't find it.
./scripts/format.sh # ruff check --fix + ruff format
./scripts/lint.sh # mypy, ty, ruff check, ruff format --checkCelery uses Redis as the broker (CELERY_BROKER_URI). In Compose, the worker runs
/start-celery-worker and Flower is exposed on :5557. The reset-password flow is
the main producer: forgot_password_action → send_password_reset_email.apply_async().
settings.BACKEND_CORS_ORIGINS/all_cors_originsare defined, butapp/main.pydoes not addCORSMiddleware— CORS is currently unenforced.scripts/prestart.shandscripts/tests-start.shreferenceapp/backend_pre_start.py/app/tests_pre_start.py, which are not present in the repo.app/initial_data.py(referenced byprestart.sh) is not present.CELERY_DATABASE_URIexists for the case where the API runs on the host but Celery runs in Docker; keep the two DB host/port settings in mind.