Case study

Smakowity Plan

Smakowity Plan (“Tasty Plan”) is a PWA that plans a family's meals for the week, generates recipes via AI, and turns them into a shopping list grouped by category.

2025–2026 design & build version 2.0.0 access on request

Request access

Context

We have two kids. My wife figured out what to cook every day — not because she couldn't, but because it's work nobody sees and that never ends. The app exists to take it off her plate: a week of meals, recipes, and a shopping list in one step, instead of a daily “what's for dinner.”

What I built

  • A weekly meal plan generated by a language model, accounting for household size and preferences.
  • A recipe under every meal: macros, ingredients with weights, preparation steps.
  • A shopping list assembled automatically from the whole week and grouped by store category.
  • A history of saved meal plans with favorites.
  • An admin panel with usage analytics and an audit log.

Crux

Technical decisions

Why a PIN instead of passwords?

This is an app for the household, not the internet — the user base is closed, there's no registration. Under that assumption a password is a cost with no upside: it has to be invented, remembered, reset. A PIN types with one hand, standing at the stove.

“Good enough for now” is a decision here, not an oversight — and that's why the rest is built so the decision can be reversed: PINs live in users.json, not in the code, and the session rests on a token signed with HMAC. Swapping out the login mechanism alone doesn't require touching the rest of the app.

Why is the rate limit per endpoint, not global?

A counter split into buckets (gemini, auth) solves a concrete problem: without it, five meal-plan generations would exhaust the limit and lock the household out of logging in. The limit protects the API key, not the family's access.

Why does the limiter let a request through when it can't write the counter?

Fail-open. When the counter store is unavailable, the request goes through — better to allow one extra request than to cut the household off from the app over a disk problem. With a limit that protects a budget, not data, that's the right trade-off.

Why doesn't the PIN comparison stop the loop on a match?

The loop runs through every PIN and uses hash_equals() instead of ===. Without it, response time would leak how many PINs had been checked before a match — and with a four-digit PIN, every hint shortens the attack.

Why does the model API key never reach the browser?

Requests go through my own PHP endpoint, which attaches the key server-side. The app can pick a model, but only from a whitelist validated on the server — the client doesn't decide what my key is used for.

Why does the key file have its own guard, when .htaccess already blocks it?

Defense in depth. config.php halts execution if it wasn't included by the app itself — that still holds even if the Apache rule disappears during a hosting migration, or someone overwrites .htaccess. Two independent layers, because the first one will fail eventually.

What it looks like

Stack

  • Vite
  • PWA / Workbox
  • React
  • PHP 8
  • Gemini 2.5 / 3.5 Flash
  • react-pdf
  • Apache / OVH

The app is locked with a PIN — it protects the API key and the household's data. Happy to set up access if you'd like to see it in action.

Request access

Back to top