2026Record
Back to work

Portfolio API

An ASP.NET Core 9 Minimal API on Cloud Firestore, containerised and deployed on Render. It serves the project list on this page — if you are reading this, it answered.

  • C#
  • .NET 9
  • Minimal API
  • Firestore
  • Docker
  • Render
Decisions

01Firestore over a relational database

Why
The managed Postgres it replaced paused on inactivity, and a paused database took the whole API down with it — including endpoints that never touched a database. Firestore's free tier does not pause.
Cost
No SQL, no joins, no versioned migrations. Document ids stopped being integers, so the public contract changed and the frontend changed with it.

02A seed failure no longer stops startup

Why
A database outage used to take down endpoints that did not need the database at all. Logging the failure and carrying on turns a total outage into a partial one.
Cost
The API can now report itself healthy while serving an empty project list.

03A dedicated /health endpoint

Why
The keep-alive ping runs 96 times a day. Pointing it at /api/projects would spend Firestore read quota on every one of them, and would fail whenever the database did — exactly when the ping needs to succeed.
Cost
One more public endpoint to maintain, carrying no data and hidden from the OpenAPI document.

04Firestore reads stay inside the C# API

Why
The front end could read Firestore directly and skip a network hop entirely. Keeping the read here preserves one place that owns data access, and keeps the C# service a real piece of work rather than a thin proxy around someone else's SDK.
Cost
The API sits on the critical path of every page load, so its cold start is the page's cold start. Cutting it out would have made the site faster and the portfolio smaller.

05The service account lives in an environment variable

Why
Render offers no convenient disk for a credential file, so the whole service account JSON is passed as a single environment variable, falling back to Application Default Credentials when it is empty.
Cost
A large secret in an env var that must never reach the repository. The .NET web SDK also treats every .json in the project as content and copies it into the published image, so the container needed its own .dockerignore to stop a local build from baking the key in.