← All posts
Engineering·

The API: FastAPI on ECS Fargate

The complete SaaS API surface: job lifecycle, tier gating, auth, billing, webhooks. And two packaging landmines you won’t find in any docs.


Plan 2 delivered the entire backend surface. Python 3.12, FastAPI, SQLAlchemy 2 async, asyncpg, Alembic migrations, passlib+bcrypt for auth, slowapi for rate limiting.

Job lifecycle — the full path

POST /api/jobs               → creates DB record, returns presigned S3 PUT URL
  Browser uploads directly → S3
POST /api/jobs/{id}/confirm  → validates upload exists, enqueues to SQS FIFO
  Lambda job_dispatcher      → SQS message → Batch submit_job
  Batch reframer container   → runs 9-module pipeline
  docker-entrypoint.sh       → POST /internal/jobs/{id}/start
                             → runs pipeline
                             → POST /internal/jobs/{id}/complete OR /fail
  webhook_delivery Lambda    → customer's configured endpoint

Tier gating

Every tier-restricted operation returns { code: "tier_limit", feature, required_plan, upgrade_url }. This is not just a guard against UI bugs — the API is the enforcement point. The UI checks tier to avoid bad UX; the API checks tier to prevent bypasses.

The bcrypt/passlib landmine

bcrypt >= 4.0 changed how it handles passwords longer than 72 bytes — it raises a ValueError. Passlib internally calls detect_wrap_bug() which hashes a 74-byte test string. This call fails with bcrypt 4. The fix is pinning bcrypt>=3.2.0,<4.0 in requirements.

You will not find this in any deprecation notice. You find it when your auth service crashes after a routine pip install --upgrade.

The pydantic EmailStr import bomb

pydantic.EmailStr requires email-validator as a separate package. If it's missing, uvicorn crashes at startup before serving a single request. The error is an ImportError deep in pydantic internals — not in your code, not at the field definition, only at runtime.

This is one of those production incidents that feels embarrassing but is completely non-obvious from reading the pydantic docs. The fix is one line in requirements.txt: email-validator.