Back to all log entries

projects · 27 August 2026

Building Kitchen Companion: From Recipe Box to Production App

I built Kitchen Companion to turn saved recipes into a practical cooking workflow, then had to solve the decidedly less glamorous problems of imports, privacy, background work and safe releases.

I started building Kitchen Companion in April with a fairly simple aim: make recipes useful beyond the moment I saved them. A bookmark or screenshot is fine until it’s Wednesday evening and I need to decide what to cook, work out what’s already in the pantry and turn the missing ingredients into a shopping list.

Four months later, Kitchen Companion is a production application rather than a digital recipe box. It manages personal recipes, weekly meal plans, pantry stock, collections and shopping lists. It can import from a web page, PDF, Word document, image or YouTube video, and it has a separate Discover library when I want inspiration rather than retrieval.

The interesting part of the build wasn’t any one of those screens. It was making the whole chain work without turning the application into a fragile collection of clever demos.

The useful bit is the chain

A saved recipe can feed a meal plan. A meal plan can generate a shopping list. Pantry stock can be deducted from that list, and ingredients can be grouped into useful categories rather than arriving as one long alphabetical trudge around the supermarket.

That sounds obvious written down, but it changes the shape of the application. Recipes need proper quantities and units. Ingredients such as “to taste” have to remain valid without quietly corrupting nutrition totals. A family plan needs shared planning while private recipe ownership still belongs to the individual who created it. Deleting or replacing a photo has to remove the right object without breaking another recipe that still refers to it.

It’s less glamorous than a flashy recipe generator, but it’s the difference between a feature and a workflow.

Importing the untidy web

Recipe websites are wonderfully inconsistent. Some publish clean Schema.org recipe data, some hide the useful bits in plugin-specific HTML, some render everything with JavaScript, and some put up an access challenge before the first ingredient appears.

Kitchen Companion uses a cheapest-first import pipeline. It looks for structured JSON-LD and known HTML patterns before involving a language model. If that still leaves gaps, it can use a small paid model through OpenRouter to normalise the result. JavaScript-heavy pages can be rendered by a local Playwright browser, while Firecrawl is an optional last resort for sites that block the normal path.

The important decision was not to send every page straight to AI. Deterministic extraction is faster, cheaper and easier to check. AI is useful for the awkward remainder, but it doesn’t get to bypass validation or invent missing quantities.

There’s also an unavoidably security-shaped wrinkle: asking a server to fetch a URL supplied by a user is a classic way to let it poke around places it shouldn’t. Every destination and redirect is checked, private and reserved addresses are rejected, and the validated DNS answer is pinned for the request. Imported images are size-limited, checked by their actual file content, decoded with pixel limits, stripped of metadata and re-encoded before they reach storage. The web is messy; importing from it safely requires a little healthy suspicion.

Slow work belongs in a queue

Early versions of this sort of application often do expensive work inside the original web request. It works beautifully in a demo and rather less beautifully when a browser takes a minute to render, an email provider hesitates or the process restarts halfway through.

Kitchen Companion now puts imports, image handling, nutrition work, categorisation, customer email and maintenance into durable jobs stored in MongoDB. A separate worker claims each job with a lease, records progress, retries bounded failures and can resume long maintenance tasks from checkpoints. The public API image stays browser-free; the worker carries Chromium and the heavier tools.

That separation costs more engineering than await doEverything() , but it gives the application a sensible answer to cancellation, retries, restarts and duplicate clicks. “Try again later” is much easier to live with when the original job hasn’t vanished into a log file.

Private means owner-scoped

Authentication is available through Google or a local email and password, with opaque server-side sessions stored in MongoDB. The more important boundary comes after login: recipes, meal plans, shopping lists, pantry data and collections are queried by owner on the server.

Administrator access only unlocks specific administrative operations. It doesn’t become a magic pass into everybody else’s kitchen. The public Discover library is held separately from personal recipes, and recipe photos sit in private S3-compatible storage behind an application route that checks the recipe, share token or family relationship before returning the image.

That rule has been worth keeping simple: hiding a button in React is a convenience; authorisation lives on the server.

The boring parts make it a product

The frontend is React, Vite and Tailwind CSS. Express and Mongoose provide the API, MongoDB holds application data and durable jobs, and MinIO stores recipe and avatar images. Stripe handles subscriptions, Notifuse and Amazon SES handle application email, and Sentry covers errors and release visibility.

What made it feel like a production system, though, was the release path. UAT and production have separate users, databases, object stores and integration settings. Each release is built into immutable API and worker images, scanned, deployed to UAT and exercised there. Production promotion uses the exact images that passed UAT rather than rebuilding something that is merely supposed to be the same.

As I write this, the current test run passes 338 server tests and 23 client tests. The live readiness check reports both the database and object storage connected on the same release. Those aren’t exciting homepage features, but they let me change the exciting ones without crossing my fingers quite so firmly.

What I took from it

The main lesson is that a useful application is a connected set of decisions. Meal planning affects ingredient structure. Importing affects security and job design. Private images affect deletion and caching. Billing affects entitlements, family access and account lifecycle. Deployment affects whether any of the above can be changed safely.

I’d make the same three broad choices again: use deterministic parsing before AI, put slow or external work into durable jobs, and treat privacy and deployment as part of the application rather than jobs to bolt on at the end.

Kitchen Companion still has edges to improve. Shopping lists currently expect a connection, for example; offline support is a documented possibility rather than a promised checkbox. That feels like the right place to be. It’s live, useful and designed to keep evolving without pretending the difficult bits have disappeared.

Kirk out.