Maritime supply chain platform

2025 to now Enterprise system FastAPI PostgreSQL Redis Celery React Azure Bicep

A maritime CRM and ERP for a ship-supply operation, built from an empty repo and owned end to end. FastAPI, PostgreSQL, Redis, React, and a Celery pipeline on AIS positions from 20,000+ vessels. Procurement is one aggregate because a vessel call is one negotiation, not five documents that happen to reference each other.

The client and the product are not named, and there are no links. The system is described by its decisions instead, which is the part worth reading anyway.

The problem

  • A real ERP and CRM for ship supply: vessels, ports, clients, suppliers, items, the chain from a vessel call to an invoice, and underneath all of it a live AIS feed of vessel traffic worldwide.
  • A system this size rots predictably: transactions half-commit, relationships lazy-load after the request is gone, one document saves while its siblings don't, a cache serves data a rolled-back write never made real.
  • Built from an empty repo, owned end to end. The aim was to make those failure modes structurally impossible, not a thing you remember to avoid.

Key decisions and tradeoffs

  • A unit of work owns every transaction; repositories never commit. SQLAlchemy errors are translated at the boundary (StaleDataError → conflict, unique violation → duplicate). Reads retry with backoff; writes never, since they aren't idempotent.
  • ORM relationships are lazy='raise'. Each read declares its eager-load graph by hand, and three test tiers prove a DTO still serialises after the session closes, so the N+1 / MissingGreenlet class of bug fails in CI rather than in production.
  • Procurement is one Job aggregate, a vessel call owning its inquiries, quotations, orders, invoices and payments. Workflow endpoints only prepare DTOs; the client sends one PATCH and an eight-step validator checks quantities, totals, invoice cumulation, allocations and FX before a single commit. Per-document REST endpoints, the obvious alternative, cannot hold the cross-document invariants.
  • Split Celery queues keep a slow AIS backfill from starving maintenance. The task source of truth is a Postgres ledger, not the Celery result backend, so history is queryable and survives eviction.
  • List caches invalidate by generation bump in a post-commit hook, never before the write lands.

What's inside

  • Modular distributed system with enforced import boundaries: routes → services → repositories → models.
  • A unit of work owns every transaction; repositories never commit; SQLAlchemy errors become domain errors at the boundary.
  • A standardised testing approach with three test tiers, totalling 4,282 tests across 288 files, catching issues in CI before they reach production.
  • Procurement as one Job aggregate: inquiry → quotation → order → invoice → payment, validated and committed atomically in a single PATCH.
  • Celery AIS pipeline on dedicated queues; a Postgres task ledger replaces the result backend as source of truth.
  • Redis-first cache; list invalidation via O(1) generation bump in a post-commit hook.
  • Defence in depth: short-lived JWT in HttpOnly cookies with refresh tokens, CSRF, IP and session-to-device binding, fail-closed RBAC plus policies.
  • Field-level audit and admin forensics: "who changed what" and "why did this vessel fail to sync" are both answerable.
  • The commercial reality modelled properly: port-wise item pricing, multi-currency with exchange rate history, unit-of-measure conversion between purchase and consumption units, delivery and payment terms per supplier and per port.

Infrastructure

  • The entire Azure environment as Bicep modules: container apps, Postgres flexible server, Redis Enterprise, container registry, private endpoints, DNS, monitoring.
  • No public database. Postgres and Redis are reachable only over private endpoints inside the VNet, with egress pinned behind a NAT gateway so partners can allow-list one address.
  • Nothing authenticates with a stored secret. Container apps pull from the registry with managed identity, and GitHub Actions signs in to Azure by workload identity federation.
  • The pipeline lints, runs static analysis, validates the Bicep, builds, deploys, and has an explicit rollback job for backend and frontend both.
  • Migrations run as Container Apps Jobs rather than inside a booting app, so a failed migration never takes the service down with it.
  • Log Analytics and Application Insights, with a custom Azure Workbook checked into the repo alongside the infrastructure.

Scale and failure modes

  • 4,282 tests across 288 files (unit, integration, API, concurrency, read-contract), with per-worker database clones for parallel runs.
  • Eight-layer middleware: CORS, session binding, CSRF, rate limiting, audit context, security headers, unified response envelope, exception handling.
  • Field-level audit and admin forensics capture provider request/response pairs, errors-only and on their own transaction, so a failed fleet sync is explainable after the fact.
  • AIS positions land in an append-only raw table with BRIN indexes on time and GIN trigram indexes for search, because the write path is chronological and the read path very much is not.
  • Celery dispatch is guarded in Redis so a retried enqueue does not run the same sync twice.
  • Deploys pinned to image digests with expand-and-contract migrations, since there is no automatic schema rollback.