Skip to content

About

TreePolitics backend — FastAPI + PostgreSQL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

AG Technology Group

API Template

CI License: Apache 2.0 Python

FastAPI template with async PostgreSQL, cookie-based JWT authentication, refresh tokens, and security hardening.

Tech Stack

Component Technology
Framework FastAPI
Database PostgreSQL (async via asyncpg)
ORM SQLAlchemy 2.0
Migrations Alembic
Auth FastAPI-Users (cookie JWT)
Rate Limiting slowapi
Package Manager uv
Containerization Docker / Docker Compose
Testing Pytest (async)
Linting/Formatting Ruff
Git Hooks pre-commit

Requirements

  • Python 3.12+
  • uv
  • Docker & Docker Compose (for local development)

Quick Start

# Clone and enter directory
cd api-template

# Copy environment file
cp .env.example .env

# Install dependencies
uv sync

# Start PostgreSQL
docker compose up -d db

# Run migrations
uv run alembic upgrade head

# Start the API
uv run uvicorn app.main:app --reload

The API will be available at http://localhost:8000

Running with Docker

# Start everything (API + PostgreSQL + Adminer)
docker compose up

# Or run in background
docker compose up -d

API Documentation

Once running, visit:

Database Viewer

Adminer is available at http://localhost:8080 when running via Docker Compose.

Login: System=PostgreSQL, Server=db, User=postgres, Password=postgres, Database=api_template

Frontend Integration

This API automatically generates an OpenAPI specification that can be used to generate type-safe clients for frontends. The companion web-template uses orval to generate React Query hooks and TypeScript types from this spec.

To generate the frontend client:

# In the frontend project run this or any similar applicable command
pnpm generate-api

This requires the API to be running locally (or set OPENAPI_URL to point to a deployed instance).

API Endpoints

Auth

Method Endpoint Description
POST /auth/register Create a new account
POST /auth/jwt/login Log in (sets access + refresh cookies)
POST /auth/jwt/logout Log out (revokes tokens, clears cookies)
POST /auth/refresh Rotate refresh token, reissue access token
GET /auth/me Get current authenticated user

Admin

Admin endpoints require the admin role (superusers also have access).

Method Endpoint Description
PATCH /admin/users/{id}/role Update a user's role

Notes (Example CRUD)

All note endpoints require authentication. Users can only access their own notes.

Method Endpoint Description
GET /notes List current user's notes
GET /notes/{id} Get a note by ID
POST /notes Create a note
PATCH /notes/{id} Update a note
DELETE /notes/{id} Delete a note

Authentication

Authentication uses httpOnly cookies with short-lived access tokens and rotating refresh tokens.

  • Access token: 15-minute JWT stored in an app_access httpOnly cookie
  • Refresh token: 7-day JWT stored in an app_refresh httpOnly cookie (scoped to /auth/refresh)
  • Token rotation: Each refresh issues a new token in the same family; reuse of an old token revokes the entire family (theft detection)
  • Rate limiting: Login (5/min), registration (3/min), refresh (30/min)

Role-Based Access Control

Users have a role field (default: user). Roles are defined as a StrEnum in app/auth/roles.py:

  • user — default role for all registered users
  • admin — can access admin endpoints (e.g. updating user roles)

Superusers (is_superuser=True) bypass all role checks. Roles are read-only via GET /auth/me and can only be changed by admins via PATCH /admin/users/{id}/role. The require_role() dependency factory can be used to gate any route:

from app.auth import require_role


@router.get("/admin-only")
async def admin_only(user: User = Depends(require_role("admin"))): ...

Security Features

  • Cookie auth: httpOnly, Secure (in production), SameSite
  • CORS lockdown: Explicit origins, methods, and headers (no wildcards in production)
  • Security headers: HSTS, X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy, Permissions-Policy
  • Rate limiting: Per-endpoint, per-client-IP limits on auth routes with security event logging (client IPs are resolved behind Cloud Run's proxy; see Client IPs behind Cloud Run)
  • Production config validation: Rejects weak secrets and default database credentials at startup
  • Security event logging: Structured logs for login, logout, registration, token refresh, and rate limit events

Logging, Telemetry & Feature Flags

Structured Logging

Logging uses structlog for structured output. In development you get colored console logs; in production, JSON.

Every request is assigned a unique X-Request-ID header (or reuses one from the incoming request), and it's automatically bound to all log entries for that request.

Configure via LOG_LEVEL env var (default: INFO).

OpenTelemetry

OpenTelemetry tracing is included but disabled by default. To enable, set OTEL_ENABLED=true and point OTEL_EXPORTER_ENDPOINT at your collector (e.g. Jaeger, Grafana Tempo). FastAPI is auto-instrumented — no code changes needed.

Analytics

app/analytics.py provides an AnalyticsBackend protocol with track() and identify() methods. The default LogAnalyticsBackend writes events to structlog. Swap it out by replacing the analytics module-level instance with your own implementation (e.g. Segment, PostHog).

Use the get_analytics() FastAPI dependency to access it in route handlers.

Feature Flags

Feature flags are read from FEATURE_* environment variables at startup (no database required). Set FEATURE_<NAME>=true or false in your .env.

The GET /flags endpoint (requires authentication) returns all flags as a JSON object, consumed by the web-template's FeatureFlagProvider.

Use the get_feature_flags() dependency in route handlers to check flags server-side via flags.is_enabled("flag_name").

Database Migrations

This project uses Alembic for database migrations.

Workflow

  1. Edit a model in app/models/
  2. Generate a migration:
    uv run alembic revision --autogenerate -m "description of change"
  3. Review the generated file in alembic/versions/ (autogenerate can miss some changes)
  4. Apply the migration:
    uv run alembic upgrade head
  5. Commit both the model change and migration file

Common Commands

# Apply all pending migrations
uv run alembic upgrade head

# Rollback one migration
uv run alembic downgrade -1

# See current migration status
uv run alembic current

# See migration history
uv run alembic history

# Generate migration without applying
uv run alembic revision --autogenerate -m "description"

Production Deployment (Cloud Run)

The API runs on Cloud Run in GCP project treepolitics-prod (region us-east1), backed by a Cloud SQL Postgres instance and Secret Manager.

Deploy a new revision (manual fallback)

Merges to main auto-deploy (see Continuous deployment below); use this manual path only for out-of-band pushes. From a clean working tree on main (set GCP_ACCOUNT to your gcloud deploy account):

GCP_ACCOUNT=you@example.com ./scripts/deploy.sh

The script builds via Cloud Build, pushes to Artifact Registry tagged with the current git short-SHA, and deploys a new Cloud Run revision pinned to that image.

Run migrations against prod

Migrations are not auto-applied on container start. Run them separately before deploying a revision that introduces schema changes:

# Terminal 1 — start the Cloud SQL Auth Proxy
cloud-sql-proxy --address 127.0.0.1 --port 5432 \
  treepolitics-prod:us-east1:treepolitics-db

# Terminal 2 — run migrations against the proxy
DATABASE_URL="postgresql+asyncpg://app:<password>@127.0.0.1:5432/treepolitics" \
  uv run alembic upgrade head

The DB user password is in Secret Manager (not committed).

Infrastructure (one-time setup, already provisioned)

  • Cloud SQL Postgres 16 (treepolitics-db, db-f1-micro, zonal, us-east1) — public IP, no authorized networks; only reachable via the Cloud SQL connector.
  • Artifact Registry docker repo api in us-east1.
  • Runtime SA treepolitics-api-runtime@treepolitics-prod.iam.gserviceaccount.com with roles/cloudsql.client and roles/secretmanager.secretAccessor (scoped to DATABASE_URL and SECRET_KEY).
  • Secrets in Secret Manager: DATABASE_URL, SECRET_KEY.
  • Custom domain api.treepolitics.net via Cloud Run domain mapping.

Client IPs behind Cloud Run

Cloud Run connects to the container from a link-local address (169.254.x.x) and passes the caller's IP in X-Forwarded-For. start.sh runs uvicorn with --forwarded-allow-ips 169.254.0.0/16 (override with FORWARDED_ALLOW_IPS), so request.client — which rate limiting and security logs use — is the real caller. uvicorn takes the right-most untrusted entry, the one Cloud Run appended, so callers can't spoof it by sending their own header. Without this, every request appears to come from the proxy and all callers share one set of rate limits.

Continuous deployment

Every merge to main auto-deploys to Cloud Run via GitHub Actions (.github/workflows/deploy.yml), so scripts/deploy.sh is only needed for out-of-band pushes.

  • Auth — Workload Identity Federation, no stored keys. The workflow mints a short-lived OIDC token that impersonates github-deployer@treepolitics-prod.iam.gserviceaccount.com (pool github-pool, provider github-provider, locked to this repo).
  • Build on the runner, not Cloud Build. The image is built with docker build on the runner and docker pushed to Artifact Registry. Cloud Build is intentionally avoided here: its log streaming requires a project-level Viewer role that the scoped deploy SA deliberately lacks.
  • Deploy. gcloud run deploy with the same flags as scripts/deploy.sh.
  • Migrations are not run in CI. Apply them first (see Run migrations against prod) before merging a schema-changing revision.

Testing

Tests use SQLite in-memory for speed and isolation.

# Run all tests
uv run pytest

# Run with verbose output
uv run pytest -v

# Run specific test file
uv run pytest tests/test_notes.py

# Run with coverage
uv run pytest --cov=app

The test harness provides test_user and other_user fixtures for testing user isolation, and an auth_client fixture that provides an authenticated HTTP client.

Linting & Formatting

This project uses Ruff for linting and formatting.

# Check for linting errors
uv run ruff check .

# Fix auto-fixable errors
uv run ruff check --fix .

# Format code
uv run ruff format .

# Check formatting without changes
uv run ruff format --check .

Git Setup & Pre-commit Hooks

Initialize git and install pre-commit hooks to auto-format on commit:

# Initialize git repository
git init

# Install pre-commit hooks
uv run pre-commit install

# Run hooks manually on all files
uv run pre-commit run --all-files

Once installed, ruff will automatically check and format your code before each commit.

Project Structure

api-template/
├── app/
│   ├── auth/
│   │   ├── backend.py          # Cookie transport + JWT strategy
│   │   ├── refresh.py          # Refresh token create/rotate/revoke
│   │   ├── roles.py            # UserRole enum + require_role() dependency
│   │   ├── security_logging.py # Structured security event logging
│   │   └── users.py            # UserManager with login/failure hooks
│   ├── models/
│   │   ├── note.py             # Note model (example CRUD entity)
│   │   ├── refresh_token.py    # Refresh token model
│   │   └── user.py             # User model (FastAPI-Users)
│   ├── routers/
│   │   ├── admin.py            # Admin endpoints (role management)
│   │   ├── auth_refresh.py     # /auth/refresh and /auth/jwt/logout
│   │   └── notes.py            # Notes CRUD (user-scoped)
│   ├── schemas/
│   │   ├── note.py             # Note request/response schemas
│   │   └── user.py             # User schemas (FastAPI-Users)
│   ├── analytics.py            # Analytics event abstraction
│   ├── config.py               # Settings with production validation
│   ├── database.py             # Async SQLAlchemy setup
│   ├── features.py             # Feature flags (env-var backed)
│   ├── logging.py              # Structlog configuration
│   ├── telemetry.py            # OpenTelemetry setup
│   └── main.py                 # App entry point, middleware, routes
├── alembic/
│   ├── versions/               # Migration files
│   └── env.py                  # Alembic configuration
├── tests/
│   ├── conftest.py             # Fixtures (client, session, users)
│   ├── test_notes.py           # Notes CRUD + isolation tests
│   └── test_roles.py           # Role-based access control tests
├── .env.example                # Environment template
├── .pre-commit-config.yaml
├── .python-version             # pyenv Python version
├── docker-compose.yml          # API + PostgreSQL + Adminer
├── Dockerfile
└── pyproject.toml

Environment Variables

Variable Description Default
DATABASE_URL PostgreSQL connection string postgresql+asyncpg://postgres:postgres@localhost:5432/api_template
SECRET_KEY JWT signing key (min 32 chars in production) change-me-in-production
ENVIRONMENT development or production development
CORS_ORIGINS Comma-separated allowed origins (production) (empty — dev uses localhost:5100-5199)
FRONTEND_URL Frontend URL for redirects http://localhost:5173
COOKIE_DOMAIN Cookie domain (leave empty for localhost) (empty)
LOG_LEVEL Logging level INFO
OTEL_ENABLED Enable OpenTelemetry tracing false
OTEL_SERVICE_NAME Service name for traces api-template
OTEL_EXPORTER_ENDPOINT OTLP gRPC collector endpoint http://localhost:4317
FEATURE_* Feature flags (e.g. FEATURE_NEW_DASHBOARD=true) (none)

License

This project is licensed under the Apache 2.0 License - see the LICENSE file for details.

About

TreePolitics backend — FastAPI + PostgreSQL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages