Field notes · v5.3
ShelfLife exists to answer one small, recurring question before it turns into a wasted trip: do we actually need this, or do we already have two at home? This page is a plain-language walkthrough of what the app does and how it's built — written for anyone curious enough to type/aboutinto the address bar.
The problem
The usual failure mode isn't forgetting to buy something — it's the opposite: buying a third jar of something because nobody was sure how many were already in the pantry. ShelfLife replaces that guess with a 60-second check before anyone leaves the house.
It's built for a household, not an inventory system — so it deliberately never asks for exact counts. Every item just has a status: enough, running low, or out.
How it behaves
Every item cycles through three states with a single tap:
That's the entire data model for "how much is left." No numbers to keep accurate, no units to disagree about — just a fast, honest gut check.
How it behaves
Items live under the same mental map as the physical space — Main Pantry, Spice Rack, Refrigerator, Freezer — each broken into a few subcategories, instead of one long alphabetical list nobody wants to scroll on a phone at 9pm.
How it behaves
Zones work well until a pantry gets big enough that finding one jar means scrolling through four of them. A search box at the top of the dashboard narrows everything down to items matching what you type, hiding whatever's left empty and opening up any collapsed zone that still has a hit. It's meant for the specific question that starts most trips — do we already have this?
How it behaves
A dedicated mode dims anything checked recently and quietly flags whatever hasn't been looked at in the last 7 days — the idea being that a fast walkthrough should spend attention on what's actually uncertain, not everything.
Mechanics
Tapping a status updates the screen instantly and writes to the shared database in the background — you're never staring at a spinner waiting for a tap to register. If a second device has the app open, it hears about that change over a live connection and updates itself within about a second, without a manual refresh.
The one subtlety worth explaining: naively combining "update instantly" with "also listen for live updates" can double-apply a change — the app would see both its own update and the live echo of that same update arriving a moment later. ShelfLife's sync logic checks whether it's already seen a given change before applying it again, so this can't happen no matter how many devices are connected at once.
Mechanics
Every screen also keeps a local copy of the pantry on the device itself. If you're checking items in a store aisle with no signal, the app still responds instantly from that local copy and quietly catches the server up once a connection comes back — nothing about the experience depends on being online at the exact moment you tap something.
Mechanics
Each household has its own name and its own password. The first time a household is created, it gets to set that password and starts with a small example layout already filled in, so there's something to look at and edit right away instead of a blank screen. From then on, both the name and password are required to get in. Typing the name in a different case later ("familyname" instead of "Familyname") still finds the same household rather than accidentally starting a second, empty one; the name you typed first is what sticks as the one everyone sees from then on.
A household stays logged in on a device across visits, the same way most apps remember you. If that household's password ever gets reset or the household gets turned off, that device gets signed out on its own — usually within under a minute — rather than quietly keeping access it shouldn't have anymore.
Mechanics
Passwords are never stored as plain text, and they're checked entirely on the server — the actual password never travels anywhere it doesn't have to, and there's no way to read one back out even from the database itself. Guessing is also a dead end: five wrong attempts in a row locks that specific account out for a few minutes, regardless of where the attempts are coming from.
There's a small, separate management view at /admin for handling households at the account level — creating one, renaming one, resetting a forgotten password, or turning a household on or off entirely. It's deliberately limited to that: there's no way to look inside a household's actual pantry from there. Anything that permanently deletes a household is tucked behind its own separate confirmation step, on purpose.
How it behaves
A toggle in the header switches between a light and dark theme — dark mode uses the same warm palette as light mode, just inverted, rather than a generic gray. The choice sticks between visits and applies instantly, no reload needed.
Under the hood
A Next.js front end styled with Tailwind, backed by a small hosted Postgres database (via Supabase) for storage and its live-update channel. It's set up as an installable app, so it can sit on a phone's home screen and open full-screen like a native app rather than a browser tab.
Zones
Main Pantry, Fridge…
Subcategories
Baking, Produce…
Items
Plenty / Low / Out
zones → subcategories → items, each level owning the next
Under the hood
Three small excerpts that show the actual mechanics described above — not the whole codebase, just the pieces that do the work.
const NEXT_STATUS = { green: "yellow", yellow: "red", red: "green" };
// tapping a status bulb just walks this ring — three states,
// one direction, nothing to type
status = NEXT_STATUS[status];function reconcile(setRows) {
return (payload) => {
setRows((prev) => {
if (payload.eventType === "DELETE")
return prev.filter((r) => r.id !== payload.old.id);
const exists = prev.some((r) => r.id === payload.new.id);
// already have this row? update it in place —
// never append a second copy of something we made ourselves
return exists
? prev.map((r) => (r.id === payload.new.id ? payload.new : r))
: [...prev, payload.new];
});
};
}// After a successful login or password setup, the server's
// response is what decides the household's name from here on —
// never whatever casing was typed in the box a moment ago.
onAuthenticated(data.household_id, { needsSeed: !!data.needs_seed });Data & privacy
Passwords are genuinely protected — hashed, checked only on the server, never readable back out. What this page still won't get into: database connection details, access credentials, or the exact mechanics behind how a household's day-to-day pantry data is kept separate from another household's. None of that changes anything described above — it's just not useful information to have sitting on an unauthenticated page.
This page isn't linked from anywhere in the app on purpose — bookmark it if you want it again.