RecipeBox, a private recipe database in three repos
RecipeBox is a recipe collection for my own dinners. It is not on the public internet, and that is on purpose. I will get to why. First, the shape of it, because the decisions behind the shape are the part I think is worth writing down.
I am a Windows and IIS admin who runs SQL Server for a living, not a developer. So the operations side of this was familiar, and the part I was learning was how to author the database and the application on top of it.
The database is disposable
The database lives in its own repo, built on SQL Server 2022. The schema is not something I edited in place and then backed up. It is a stack of numbered migration files, a couple dozen of them at the moment, applied in order. Recipes live as JSON files in the same repo.
The rule I wrote at the top of its README is that nothing in SQL Server is the source of truth for anything. One script tears the database down, applies every migration, loads every recipe and photograph, and rebuilds the ingredient vocabulary. It is how I normally work, not an emergency procedure.
That is a strange idea for someone who has spent years protecting production databases. But it
changes what a mistake costs. If I get a migration wrong, I rebuild. The destructive reset
script even refuses to run against anything not named RecipeBox, and refuses while recipes are
still in the tables, so the safety is in the tool and not in my memory.
One honest exception: the photographs are gitignored, so they exist only in my working copy and in the database. A fresh clone elsewhere rebuilds the recipes but not the pictures. That is in my backlog as a known gap, not a solved problem.
The app only runs stored procedures
The web app is ASP.NET Core Razor Pages with Dapper, a thin library that maps query results onto plain C# classes. There is no Entity Framework, because the stored procedures already exist.
Every read the app does goes through a stored procedure. That is not mainly about tidiness. It
means the database login the site uses can have CONNECT and EXECUTE on four procedures and
nothing else, with no table access at all. If the app is ever compromised, or I write a bad
query, the login cannot read a table directly.
I did not want to just claim that, so there is a script that tests it. It confirms the login can run the procedures and is refused on every direct read. Writing that test turned up a real defect in one of my migrations, which is a post of its own.
Three repos, and requests between them
The work is split by who owns what. The database has a repo, the application has a repo, and the server infrastructure (containers, reverse proxy, firewall) has a repo.
The app repo owns the application and nothing else. If it needs a new stored procedure, or a
change to the hosting, it does not reach into the other repo. It writes a numbered request
document describing what it needs and why, and I carry that to the repo that owns the change.
Those requests sit in the app repo as db-requests, infra-requests and site-requests, with
a status when they close.
This feels slower than one repo with everything in it, and it is. The benefit is that each repo describes only what it owns, and the seams are written down instead of living in my head. The Dockerfile, for example, lives with the infrastructure, because Docker only runs on my home server. That creates one coupling worth noting: the app's target framework has to match the image tags in the Dockerfile, and a note in the app repo says so.
Private and LAN-only, deliberately
Much of the collection is recipes fetched from the web, kept as text for my own reference. That
material is marked in the database so it cannot be flagged as published, and the fetcher reads
and obeys each site's robots.txt. The rules around that deserve their own post.
What actually keeps the site private is simpler. It runs in a container on my home server behind my reverse proxy, and it is not reachable from outside my network. That is a decision, not a missing feature. Public hosting is a different decision, and I would want real authentication in front of it first.
One consequence of living on that server: it powers off overnight. The app handles this by serving a plain "database unreachable" page that names the window, instead of a raw error. A permission error, by contrast, is left as a loud failure on purpose, so a missing grant is never mistaken for an outage.
The static attempt I reverted
My first try at putting recipes on a website was a static one, built with Eleventy in this very repo. I reverted it. The database had been designed for a live consumer all along. The browse and filter procedures only make sense when something asks them a question at request time. So the recipes became a dynamic app, and this blog went back to being just posts.
Where it stands
It went live on my home network on 30 July 2026, and served 142 recipes at that point. The browse page filters through links rather than JavaScript, and the site uses no JavaScript at all. I did that deliberately, to see how far plain HTML gets you.
The ingredient parser, the fetching rules and the permission test each have more to say, so those are next.