Shared API backend for Tripod — a platform powering multiple language and translation tools. Handles authentication (JWT), app-scoped RBAC, and core data (languages, projects, organizations, phases).
Stack: FastAPI · SQLAlchemy 2 async · Alembic · PostgreSQL (Neon) · uv · Docker · Cloud Run
app/
├── api/ # FastAPI routers (one file per domain: auth, languages, orgs, projects, phases, roles)
├── core/ # Config, database engine, middleware, exceptions
├── db/
│ └── models/ # SQLAlchemy ORM models, one file per domain:
│ # auth.py · language.py · org.py · phase.py · project.py
├── models/ # Pydantic request/response schemas, mirroring db/models structure
└── services/ # Business logic, one package per domain:
# auth/ · authorization/ · language/ · org/ · phase/ · project/
# Each package exposes one function per file.
alembic/ # Database migrations
scripts/ # One-off scripts (e.g. seed_apps_roles.py)
tests/ # Async pytest suite, one file per service domain
http/ # .http request examples (VS Code REST Client / JetBrains)
Each layer has a single responsibility: routers call services, services use db models, Pydantic models handle serialization. No business logic lives in routers; no DB calls live outside services.
| Trigger | What happens |
|---|---|
| Pull request | ruff check + ruff format + pytest |
Push to main |
Build image → alembic upgrade head → deploy to Cloud Run |
Secrets required in GitHub Actions (Settings → Secrets):
| Secret | Purpose |
|---|---|
GCP_PROJECT_ID |
Artifact Registry & Cloud Run project |
GCP_SA_KEY |
Service account JSON (Cloud Run Admin, Artifact Registry Writer, Secret Manager Accessor) |
SECRETS_PROJECT_NUMBER |
Project number for Secret Manager (tripod_backend_neon_database_url, tripod_backend_jwt_secret) |
Config is pulled from GCP Secret Manager at container startup — no env vars are set manually in production or CI.
Secrets are fetched from GCP Secret Manager via a sidecar (gcp-secrets) on startup.
# 1. Authenticate
gcloud auth application-default login
# 2. Start the stack (SECRETS_PROJECT_ID tells the sidecar which GCP project to read secrets from)
SECRETS_PROJECT_ID=<id> docker compose up --build backend
# 3. In another terminal — apply migrations, seed, run tests
docker compose exec backend sh -c "set -a && . /run/secrets/.env && set +a && uv run alembic upgrade head"
docker compose exec backend sh -c "set -a && . /run/secrets/.env && set +a && uv run python scripts/seed_apps_roles.py"
docker compose exec backend sh -c "set -a && . /run/secrets/.env && set +a && uv run pytest tests"Use a Neon dev branch for local work to avoid touching production data.
# Create (after changing models)
docker compose exec backend sh -c "set -a && . /run/secrets/.env && set +a && uv run alembic revision --autogenerate -m 'short description'"
# Apply locally (manual — never run automatically against local DB)
docker compose exec backend sh -c "set -a && . /run/secrets/.env && set +a && uv run alembic upgrade head"Production migrations run automatically on deploy (after merge to main).
uv run ruff check . --fix
uv run ruff format .Or via Docker: docker compose --profile lint run --rm lint
http/ contains .http request files for health, auth, and roles. See http/README.md for token usage.