A modular FastAPI backend for user authentication and todo management. The project is structured as a modulith and follows the main ideas of Domain-Driven Design, Clean Architecture, and CQRS:
- Each business module owns its presentation, application, domain, and infrastructure code.
- FastAPI routers stay at the delivery edge.
- Application handlers coordinate use cases.
- Domain entities and repository contracts define business concepts.
- Infrastructure repositories adapt SQLAlchemy models to domain entities.
The API is currently versioned under /api/v1.
- Features
- Tech Stack
- Project Structure
- Architecture
- Request Flow
- API Routes
- Prerequisites
- Environment Variables
- Local Setup
- Running the Application
- Database and Migrations
- Testing and Quality Checks
- Makefile Commands
- Additional Documentation
- Docker Notes
- Development Guide
- Troubleshooting
- Security TODO
- Known Notes
- User registration.
- User login with JWT access and refresh tokens.
- Refresh token rotation and logout/token revocation.
- Authentication middleware that validates bearer tokens.
- RBAC authorization with Casbin-backed roles and permissions.
- Current-user lookup from authenticated request state.
- Todo creation.
- Cursor-paginated todo listing by authenticated user.
- Todo update with ownership checks.
- Todo delete with ownership checks.
- Role and permission management APIs.
- Redis-backed rate limiting.
- Request ID, structured logging, audit logging, and security headers.
- Request size limiting and idempotency support.
- Health, liveness, and readiness endpoints.
- API route grouping under
/api/v1. - Async SQLAlchemy persistence.
- Alembic database migrations.
- Database seeders for authorization data and optional users.
- Pytest application-validation tests.
- Ruff linting.
- Python
>=3.14 - FastAPI
- Uvicorn
- SQLAlchemy async ORM
- Asyncpg PostgreSQL driver
- PostgreSQL
- Alembic
- Pydantic and Pydantic Settings
- Python JOSE for JWT handling
- Passlib for password hashing
- Casbin for authorization policies
- Redis and fastapi-limiter for rate limiting and token revocation
- Pytest
- Ruff
- Poetry
.
├── alembic/ # Alembic migration environment and versions
├── scripts/ # Helper shell scripts
├── src/
│ ├── main.py # FastAPI application entrypoint
│ ├── core/
│ │ ├── bootstrap/ # Application bootstrap helpers
│ │ ├── authorization/ # Shared authorization persistence and services
│ │ ├── config/ # Runtime settings
│ │ ├── database/ # PostgreSQL and Redis connection setup
│ │ ├── dependency/ # Shared FastAPI dependencies
│ │ ├── exceptions/ # Exception registration and handlers
│ │ ├── middleware/ # Auth, audit, security, logging, request middleware
│ │ ├── routers/ # API router composition
│ │ ├── schemas/ # Shared response schemas
│ │ ├── security/ # JWT, password, revocation, audit helpers
│ │ ├── seed/ # Database seeding orchestration
│ │ └── lifespan.py # FastAPI lifespan hook
│ ├── modules/
│ │ ├── authorization/
│ │ │ ├── application/ # Role and permission use cases
│ │ │ ├── domain/ # Role, resource, and permission entities
│ │ │ ├── infrastructure/ # Casbin and SQLAlchemy adapters
│ │ │ └── presentation/ # Role and permission routers/schemas
│ │ ├── user/
│ │ │ ├── application/ # User commands, queries, handlers
│ │ │ ├── domain/ # User entity, exceptions, repository port
│ │ │ ├── infrastructure/ # SQLAlchemy model/repository/services
│ │ │ └── presentation/ # FastAPI router, dependencies, schemas
│ │ └── todo/
│ │ ├── application/ # Todo commands, queries, handlers
│ │ ├── domain/ # Todo entity, exceptions, repository port
│ │ ├── infrastructure/ # SQLAlchemy model/repository
│ │ └── presentation/ # FastAPI router and dependencies
│ └── shared/
│ ├── database/ # Shared SQLAlchemy base and mixins
│ ├── email/ # Shared email contracts
│ ├── events/ # Shared event contracts
│ ├── exceptions/ # Cross-cutting exceptions
│ └── utils/ # Cursor pagination helpers
├── templates/emails/ # Transactional email templates
├── tests/ # Pytest tests
├── pyproject.toml # Project metadata and dependencies
├── poetry.lock # Poetry lock file
├── alembic.ini # Alembic configuration
├── Dockerfile # Docker image definition
└── docker-compose.yml # Local API, PostgreSQL, and Redis services
This is a single deployable application with module boundaries inside the codebase. The user, todo, and authorization modules are independent feature areas under src/modules.
The intended dependency direction is:
presentation -> application -> domain
infrastructure -> domain
The domain layer should not depend on FastAPI, SQLAlchemy, or external infrastructure. Infrastructure implements domain repository contracts.
Each module follows this shape:
domain: entities, domain exceptions, repository interfaces.application: commands, queries, and handlers that coordinate use cases.infrastructure: SQLAlchemy models and repository implementations.presentation: FastAPI routers, request/response schemas, and dependency wiring.
The code separates commands and queries at the application naming level:
- Commands mutate state, for example register user, login user, create todo, update todo.
- Queries read state, for example user detail and todo listing.
Some flows are still pragmatic and can be made stricter over time by introducing dedicated read repositories or read DTOs.
POST /api/v1/auth/register
-> user_router.register
-> RegisterUserCommand
-> RegisterUserCommandHandler
-> UserRepository port
-> SQLAlchemyUserRepository
-> users table
POST /api/v1/auth/login
-> user_router.login
-> LoginUserCommand
-> LoginUserCommandHandler
-> SQLAlchemyUserRepository
-> JWT access token
Request with Authorization: Bearer <token>
-> AuthenticationMiddleware validates JWT
-> request.state.user_id is set
-> route dependency checks role permission
-> todo handler executes use case
-> TodoRepository port
-> SQLAlchemyTodoRepository
-> todos table
Base API prefix:
/api/v1
Current routes:
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
GET /api/v1/auth/me
POST /api/v1/auth/logout
POST /api/v1/todos/
GET /api/v1/todos/?cursor=<cursor>&limit=10
GET /api/v1/todos/{todo_id}
PATCH /api/v1/todos/{todo_id}
DELETE /api/v1/todos/{todo_id}
POST /api/v1/roles/
GET /api/v1/roles/?cursor=<cursor>&limit=10
GET /api/v1/roles/{role_id}
PATCH /api/v1/roles/{role_id}
DELETE /api/v1/roles/{role_id}
POST /api/v1/roles/{role_id}/permissions/{permission_id}
DELETE /api/v1/roles/{role_id}/permissions/{permission_id}
POST /api/v1/permissions/
GET /api/v1/permissions/?cursor=<cursor>&limit=10
GET /api/v1/permissions/{permission_id}
PATCH /api/v1/permissions/{permission_id}
DELETE /api/v1/permissions/{permission_id}
GET /health
GET /live
GET /ready
Public routes:
/health/live/ready/docs/redoc/openapi.json/api/v1/auth/login/api/v1/auth/register/api/v1/auth/refresh
Protected routes require:
Authorization: Bearer <access_token>
Swagger UI, ReDoc, and OpenAPI JSON are disabled when APP_ENV=production.
- Python
3.14or compatible with the project constraint. - Poetry.
- PostgreSQL, either local or via Docker.
- Redis, either local or via Docker.
- Docker with the Compose plugin, if using the containerized stack.
- Make, if using the generated
Makefile.
Create a .env file from .env.example:
cp .env.example .envExpected values:
APP_NAME=Todo Modulith API
APP_ENV=production
FRONTEND_URL=http://localhost:3000
POSTGRES_USER=postgres
POSTGRES_PASSWORD=
POSTGRES_DB=todo_db
REDIS_PASSWORD=
DATABASE_URL=
DATABASE_POOL_SIZE=20
DATABASE_MAX_OVERFLOW=10
DATABASE_POOL_TIMEOUT=30
DATABASE_POOL_RECYCLE=3600
REDIS_URL=
SECRET_KEY=
MAX_REQUEST_SIZE_MB=5242880
ALGORITHM=HS256
JWT_ISSUER=todo-modulith-api
JWT_AUDIENCE=todo-modulith-client
ACCESS_TOKEN_EXPIRE_MINUTES=30
REFRESH_TOKEN_EXPIRE_MINUTES=10080
RATE_LIMIT=100/minute
CORS_ALLOW_ORIGINS=http://localhost:3000
CORS_ALLOW_METHODS=*
CORS_ALLOW_HEADERS=*
SECURITY_CONTENT_SECURITY_POLICY=default-src 'self'; frame-ancestors 'none'
IDEMPOTENCY_TTL_SECONDS=86400
ACCOUNT_LOCKOUT_MAX_ATTEMPTS=5
ACCOUNT_LOCKOUT_WINDOW_MINUTES=15
ACCOUNT_LOCKOUT_DURATION_MINUTES=15
LOG_FORMAT=json
EMAIL_PROVIDER=ses
AWS_REGION=us-east-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
SES_FROM_EMAIL=noreply@example.com
SENDGRID_API_KEY=
SENDGRID_FROM_EMAIL=noreply@example.com
SMTP_HOST=
SMTP_PORT=587
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_EMAIL=noreply@example.com
SMTP_USE_TLS=true
SEED_ADMIN_EMAIL=
SEED_ADMIN_PASSWORD=
SEED_ADMIN_USERNAME=admin
SEED_ADMIN_FULLNAME=System Administrator
SEED_DEVELOPMENT_USERS_PASSWORD=MAX_REQUEST_SIZE_MB is currently interpreted as a byte count despite its name. Keep it at 5242880 for a 5 MiB limit.
For local development without Docker, use development mode and point the service URLs at local PostgreSQL and Redis instances, for example:
APP_ENV=development
DATABASE_URL=postgresql+asyncpg://postgres@localhost:5432/todo_db
REDIS_URL=redis://127.0.0.1:6379/0Production mode requires a non-default SECRET_KEY, non-empty database and Redis URLs, JWT issuer and audience values, positive token lifetimes, and explicit CORS origins.
The Makefile expects Poetry to create .venv inside the repository. Configure that once, then install dependencies:
poetry config virtualenvs.in-project true --local
poetry installRun commands through Poetry directly:
poetry run pytest -qThis repository also has a local .venv, so the Makefile uses .venv/bin/... where practical.
Run the API locally:
poetry run uvicorn src.main:app --reload --host 0.0.0.0 --port 8000Or with Make:
make runOpen:
http://localhost:8000/docs
This documentation endpoint is available only when APP_ENV is not production.
Health check:
http://localhost:8000/health
Operational checks:
http://localhost:8000/live
http://localhost:8000/ready
Alembic is configured in:
alembic.inialembic/env.pyalembic/versions/
Apply migrations:
poetry run alembic upgrade headCreate a new migration with autogenerate:
poetry run alembic revision --autogenerate -m "describe change"Rollback one migration:
poetry run alembic downgrade -1With Make:
make migrate
make seed
make revision name="add todo due date"
make downgradeImportant: migration autogeneration depends on importing all SQLAlchemy models in alembic/env.py, so new module models must be imported there or through a central model registry.
Seed baseline records after applying migrations:
make seedThe seeder runs all changes in one transaction and is idempotent. It creates the default authorization resources, roles (admin, user, manager, and viewer), permissions, role-permission links, and matching Casbin policies without duplicating existing records. If any seed operation fails, the transaction is rolled back.
To seed an initial admin user, set these environment variables before running make seed:
SEED_ADMIN_EMAIL=admin@example.com
SEED_ADMIN_PASSWORD=
SEED_ADMIN_USERNAME=admin
SEED_ADMIN_FULLNAME=System AdministratorFor each new seeded user, the repository creates records that follow the normalized user schema:
usersstores email, username, password hash, authentication provider, and status.user_profilesstoresSEED_ADMIN_FULLNAME(or the demo account name) asdisplay_name.user_settingsstores the default language, timezone, theme, and notification preferences.user_securitystores the default login-attempt, lockout, password, and two-factor state.user_has_rolesassociates the user with its seeded role, with a matching Casbin grouping policy.
SEED_ADMIN_FULLNAME is retained for configuration compatibility; it does not refer to a users.fullname column. The normalized schema stores this value in user_profiles.display_name.
If SEED_ADMIN_EMAIL or SEED_ADMIN_PASSWORD is empty, admin-user seeding is skipped. If a seeded email already exists, the seeder does not change that user's identity, password, profile, settings, security state, or roles.
When APP_ENV=development, the seeder can also create demo users with different roles. Set a shared development password before running make seed:
SEED_DEVELOPMENT_USERS_PASSWORD=Development demo accounts:
user@example.comwith display nameDefault Userand theuserrolemanager@example.comwith display nameTodo Managerand themanagerroleviewer@example.comwith display nameTodo Viewerand theviewerrole
Demo users are skipped outside development and are not modified when their email already exists.
Run tests:
make testRun lint:
make lintRun the full local check:
make checkCurrent check set:
pytest -qruff check src tests scripts- import boundary checks through
lint-imports - import check for
src.main
The current pytest suite focuses on application-layer command and query validation. Broader middleware, API integration, and infrastructure regression coverage still needs to be added.
Dependency scanning is available separately:
make security-scanmake help
make install
make run
make test
make lint
make lint-imports
make import-check
make security-scan
make check
make migrate
make seed
make downgrade
make revision name="migration message"
make db-up
make db-down
make db-logs
make cleanBefore starting Docker Compose, set non-empty POSTGRES_PASSWORD, REDIS_PASSWORD, and SECRET_KEY in .env. Compose intentionally fails fast when database or Redis passwords are missing.
Run API, PostgreSQL, and Redis services:
make db-upThe API container applies Alembic migrations before starting Uvicorn. Database seeding remains an explicit make seed step.
Stop services:
make db-downFollow service logs:
make db-logs- Add a command or query in the module application layer.
- Add a handler in the application layer.
- Keep business rules in the domain entity when they are true invariants.
- Depend on domain repository interfaces, not SQLAlchemy directly.
- Add or extend infrastructure repositories only in the infrastructure layer.
- Wire the handler in presentation dependencies.
- Expose the use case from the FastAPI router.
- Add focused tests.
Use the same structure:
src/modules/<module_name>/
├── application/
├── domain/
├── infrastructure/
└── presentation/
Then register its router in:
src/core/routers/api/v1.py
-
Create the SQLAlchemy model in the module infrastructure layer.
-
Ensure the model imports into Alembic metadata discovery.
-
Generate a migration:
make revision name="add new table" -
Review the generated migration before applying it.
-
Apply:
make migrate
The tests add both the repository root and src to sys.path through tests/conftest.py. If new tests import modules inconsistently, prefer absolute src... imports.
Check DATABASE_URL.
For Docker Compose, the database hostname is usually:
db
For local execution against a host PostgreSQL instance, it is usually:
localhost
Make sure the model is imported by alembic/env.py or by something that is imported there before target_metadata = Base.metadata.
Protected routes require:
Authorization: Bearer <token>
The token must contain a sub claim with a valid user id.
Legend: Implemented means code exists in the repository. Partial means code exists but still needs a fix, test, or production hardening.
| Category | Recommended | Current Status | Notes |
|---|---|---|---|
| JWT Authentication | Required | Implemented | AuthenticationMiddleware validates bearer tokens for non-public routes. |
| Refresh Token Rotation | Required | Implemented | Refresh flow revokes the old refresh token and persists a new token. |
| RBAC + Permissions | Required | Implemented | Casbin-backed role and permission checks are wired through route dependencies. |
| Rate Limiting (Redis-backed) | Required | Implemented | Redis-backed limiter reads the configured RATE_LIMIT value. |
| Security Headers Middleware | Required | Implemented | Adds X-Content-Type-Options, X-Frame-Options, CSP frame-ancestors, Referrer-Policy, and Permissions-Policy. |
| CORS Configuration | Required | Implemented | CORS origins, methods, and headers are environment-driven through settings. |
| Request ID Middleware | Required | Implemented | Generates or propagates X-Request-ID and stores it on request state. |
| Audit Logging | Required | Implemented | Adds global endpoint audit logging, domain audit events, and separate persisted error traces. |
| Structured Logging | Required | Implemented | Logs request ID, method, path, status, latency, and user context when available. |
| Global Exception Handling | Required | Implemented | Domain exceptions are registered explicitly and Exception is used only as the fallback handler. |
| Input Validation | Required | Implemented | Pydantic schemas and application validation functions are used across user and todo flows. |
| Password Hashing (Argon2 or bcrypt) | Required | Implemented | User auth service uses bcrypt hashing. |
| Account Lockout | Required | Implemented | Tracks failed logins and temporarily locks accounts after configured thresholds. |
| Token Revocation | Required | Implemented | Refresh tokens are revoked on rotation/logout, and access tokens are denylisted in Redis until expiry. |
| OpenAPI Authentication | Required | Implemented | Swagger OAuth2 auth is configured, and docs/OpenAPI endpoints are disabled when APP_ENV=production. |
| Health Check Endpoint | Required | Implemented | /health endpoint returns service health. |
| Readiness/Liveness Endpoints | Required | Implemented | Adds /live and /ready operational endpoints. |
| Request Size Limiting | Required | Implemented | LimitRequestSizeMiddleware rejects oversized write requests. |
| Idempotency Support (for applicable POST endpoints) | Optional but valuable | Implemented | Supports Idempotency-Key replay caching for POST responses. |
| Database Migrations | Required | Implemented | Alembic is configured with migration commands in the README and Makefile. |
| Dependency Injection | Required | Implemented | FastAPI dependencies wire repositories, handlers, auth, authorization, and database sessions. |
| Configuration via Environment Variables | Required | Implemented | Pydantic settings read .env and reject the default secret key in production. |
- Fix and verify rate limit configuration wiring.
- Add security headers middleware.
- Add request ID middleware.
- Add structured request logging.
- Add audit logging for sensitive actions.
- Add account lockout or equivalent failed-login protection.
- Disable or authenticate
/docs,/redoc, and/openapi.jsonin production. - Add readiness and liveness endpoints.
- Add production config validation for secrets and unsafe defaults.
- Harden CORS through environment-driven allowed origins, methods, and headers.
- Review exception responses to avoid leaking token parsing details or internal exception messages.
- Add automated tests for request size limits, rate limiting, auth failures, authorization failures, CORS, security headers, and request IDs.
- Add dependency vulnerability scanning to local or CI checks, for example
pip-auditor an equivalent Poetry-compatible scanner.
- The automated test suite currently covers application validation only; middleware, router, database, Redis, and authorization integration paths are not covered.
- Authorization persistence currently exists under both
src/core/authorization/infrastructureandsrc/modules/authorization/infrastructure; new work should avoid increasing that duplication. - Some authorization routes call the domain service directly while other modules use dedicated application handlers, so CQRS boundaries are not yet applied consistently.