Case study
Task Management API
A containerised REST API where cross-user access is structurally impossible, slow work runs in the background, and the AI endpoint is tested for being right, not only well-formed.
The problem
A task API is only interesting once more than one person uses it. Then three things get hard: proving who the caller is, guaranteeing that a caller can never touch another user's rows, and doing slow work without making the caller wait for it. All three are easy to get almost right — and “almost right” on an authorisation boundary is indistinguishable from broken.
What I did & decided
- Split the service into Controllers → Services → Repositories, with storage behind one interface
(
ITasksRepository, implemented over EF Core and PostgreSQL). - Made Supabase the identity provider and this API a pure resource server: it never stores a password
and never signs a token, it only verifies them — in
SupabaseAuthMiddleware, so the controllers contain no authentication code at all. - Chose a network round-trip over a local signature check. It is slower, but a revoked token is rejected on the next request instead of staying valid until it expires. When Supabase is unreachable the API returns 503, not 401: an outage is not the caller's fault.
- Made ownership a required parameter on every repository query, taken from the verified token, never from the request body. Asking for someone else's task returns 404, not 403 — a 403 would confirm that the id exists.
- Moved slow work out of the request:
POST /api/reportsanswers 202 with a status URL in milliseconds, and Hangfire runs two chained steps with two retries and 5 s / 10 s backoff. A report whose retries run out is markedfailed, never leftpending, and a cron heartbeat every minute flags anything stuck for more than two minutes. - Added an AI endpoint,
POST /api/tasks/draft, that turns a messy note into a suggested task. The note travels in the user message, never inside the system prompt (the prompt-injection defence); invalid output gets one repair attempt, then a 422. A provider outage is a 503 withRetry-After, because those two failures need different reactions from the client.
What came of it
- Cross-user access is structurally impossible, not policed: the query cannot be written without an owner.
- Three PowerShell scripts check the README's claims against a running instance: 16 checks for authentication and ownership, 19 for the background jobs, and the AI checks.
- A report measured 8.2 seconds from creation to completion — after the request that started it had already returned.
- The eval suite scored 6 of 8 cases fully correct (18 of 20 checks). Both failures are date arithmetic, so the README says what I would change: resolve dates in C#, not in the model.
docker compose upstarts the API (with its job worker) and PostgreSQL 16 together.
Next case: Car Rental Management System