Modeling a CMS on Firestore After Years of Relational Databases
NoSQL doesn’t remove the need for a schema - it just moves the enforcement from the database to your code.

Most of my backend work has been on relational databases, so building this portfolio's CMS on Firestore meant unlearning a few habits before I could use it well.
No Joins Means No Joins
The first adjustment is obvious but easy to underestimate: there's no query that pulls a project and its related media in one round trip the way a SQL join would. Every piece of content — projects, artwork, blog posts — stores its own media references directly as embedded objects (a URL, a media ID, alt text) rather than pointing at rows in a separate table. Denormalizing like that felt wrong at first, coming from a relational background where duplication is usually a smell. In a document database, it's often just how reads stay fast.
The Schema Still Exists - It's Just in TypeScript
A relational database enforces its schema for you; Firestore enforces nothing. The schema still exists in this project, just one layer up - as TypeScript interfaces and Zod validators that every write passes through before it reaches the database. Every Project, Artwork, and Blog document has a corresponding Zod schema in the validators layer, and the repository layer is the only code allowed to talk to Firestore directly. Nothing writes to a collection without going through that gate first.
Designing for "Which Fields Exist, Not Just Which Are Filled"
The other adjustment was building for optional, toggleable content sections - a project might have an architecture write-up or might not, a case study might show performance metrics or might not. Rather than letting the frontend guess from empty strings, each content type carries its own `sections` map recording which parts are actually meant to be shown. That turned out to be the right NoSQL instinct: store the shape of the content explicitly, since there's no schema migration tool to fall back on later if a field's meaning quietly changes.
