Building the Home Kitchen Management App for Organized Foodies

Published on May 15, 2025

Table of Contents

The What? Why?

I’m building Where’s the Garlic?!, the (I say this like it’s going to change the course of human evolution) home kitchen management app for organized foodies to solve two distinct pain points for my partner and I:

  1. We would constantly make it to the grocery store, then she turns to ask me how we are on [insert grocery item here] and I’d be like [image confused face and “idk” arms up].

  2. We have many of our favorite recipes and we cook a lot for others. We sometimes have trouble tracking and sharing recipes with family and friends.

As someone mildly organized in a relationship with someone who is wildly organized, I figured I’d have fun building an app that keeps us organized with ease.

There will be many challenges along the way, I’m sure, but I really want to build this in public, help others stay organized, and see where the adventure leads.

Why I’m Writing This

I’m writing this to document the journey and the challenges along the way to take others on the ride of going from idea to product. I’m building in public.

As a marketer and a software developer, I feel like I strike that sweet point between the technical and non-technical. So I want this to be a space where someone who doesn’t know how to build software but just has a curiosity can follow the logic and process without getting bogged down in jargon.

I like to keep things simple and effective. Complications, to me, are usually a bi-product of a lack of understanding, not a feature.

I’ll be continually updating this post as the app grows and the journey goes.

Thanks for taking the ride with me. Here we go.

Building for Immediate Interactivity

After some exploring on Reddit, I found a subreddit that led me to a very interesting insight: people often turn away from applications they need to sign up to use. And it doesn’t matter how quick and easy that sign up process is.

With an app like this, where it’s a personal optimization tool, putting an authentication process in front of it means that I might be turning away curious people who might otherwise love the app because they don’t get to experience anything before I present them with authentication.

When building, I try to make user experience a critical factor because that very much impacts the product’s potential for usage.

The challenge here is in maintaining the state of user interactions. Unlike with a tool (think something like a file converter) where you totally expect to lose whatever you added into the app on browser refresh, this is a platform. So, for anyone using it, they might start playing with it now and then close their browser for some reason. They shouldn’t have to start from scratch.

So the question is how do we store the data that people create while using the app without authentication because without that, we don’t want them to be able to enter anything into the production database.

That’s where IndexedDB comes in. It’s like a tiny database that lives right in your web browser, which allows for saving data locally without needing to sign up. Even better than discovering IndexedDB was discovering Dexie, I wanted to be able to move quickly without needing to soak up all of the syntax commonly associated with web APIs.

The goal here is to give a just-about-full app experience without needing to sign in. This means I need to store values and, because some users will be able to switch across households, I also needed to manage context above the app, which would allow for appropriate refreshes on data based on the active household chosen by the user.

Setting Up Local Database Access

Luckily, Dexie allowed me to move very quickly on this:

import Dexie, { type EntityTable } from "dexie";
import type { Tables } from "@/database.types";

// Generated from my database
// (the docs point to a DIY type, more on that later)

/*
type Household = {
    created_at: string;
    creator_id: string;
    id: string;
    title: string;
    updated_at: string;
}
*/
type Household = Tables<"households">;

// Creates a new database named WTGarlic
// Adds a table named households with the type and id as the primary key
const dexie = new Dexie("WTGarlic") as Dexie & {
  households: EntityTable<Household, "id">;
};

// Tells Dexie which columns to build
dexie.version(1).stores({
  households: "id, title, created_at, creator_id, updated_at",
});

export type { Household };
export { dexie };

This allowed me to add a simple user based action:

const onSubmit = async (values: z.infer<typeof formSchema>) => {
  try {
    if (user) {
      await createHouseholdMutation.mutateAsync(values);
    } else {
      await dexie.households.add({
        id: crypto.randomUUID(),
        created_at: new Date().toISOString(),
        creator_id: "local-user",
        title: values.name,
        updated_at: new Date().toISOString(),
      });
    }
    onSuccess();
  } catch (error) {
    console.error("Failed to save household:", error);
  }
};

Creating Context on Active Households

This is where things get good. Once the household is added, we want to be able to identify the initial household as the active one.

Just about everything in this application will be dependent on which household is active. You don’t want to manage items for one household while active in another. And when you change households, you’ll want all of the app data to update to be relevant to that household.

This is where React Context came in for a big win. Here’s how I used it to give the entire app context on the active household:

import { createContext, useContext, useState, useEffect } from "react";
import { useUser } from "@/hooks/useUser";
import { dexie } from "@/dexie-db";
import { api } from "@/utils/trpc";
import { useQuery } from "@tanstack/react-query";
import { useLiveQuery } from "dexie-react-hooks";
import type { Tables } from "@/database.types";

type Household = Tables<"households">;

interface ActiveHouseholdContextType {
  activeHousehold: Household | null;
  setActiveHousehold: (household: Household | null) => void;
  households: Household[];
  isLoading: boolean;
}

const ActiveHouseholdContext = createContext<ActiveHouseholdContextType | null>(
  null,
);

export function ActiveHouseholdProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const { data: user } = useUser();
  const [activeHousehold, setActiveHousehold] = useState<Household | null>(
    null,
  );

// Using tRPC here to get back the query, and stating to only enable if there is a user
  const { data: remoteHouseholds = [], isLoading: isLoadingRemote } = useQuery({
    ...api.household.list.queryOptions(),
    enabled: !!user,
  });

// Gets households from the local database
  const localHouseholds = useLiveQuery(() => dexie.households.toArray()) || [];
  const isLoadingLocal = !localHouseholds;

// Determines these based on whether a user exists
// If they do, give me remotes, if not, give me the locals
// Still takes advantage of loading state from TanStack query on remote calls
  const households = user ? remoteHouseholds : localHouseholds;
  const isLoading = user ? isLoadingRemote : isLoadingLocal;

// For right now, we just want the first one to be active
// This use effect is dependent on households, which is changed when we add the household to the db, triggering the effect, which changes the active household
  useEffect(() => {
    if (!activeHousehold && households.length > 0) {
      setActiveHousehold(households[0]);
    }
  }, [households, activeHousehold]);

// Returns the values from the provider
  return (
    <ActiveHouseholdContext.Provider
      value={{
        activeHousehold,
        setActiveHousehold,
        households,
        isLoading,
      }}
    >
      {children}
    </ActiveHouseholdContext.Provider>
  );
}

// Creates the hook to access these values
export function useActiveHousehold() {
  const context = useContext(ActiveHouseholdContext);
  if (!context) {
    throw new Error(
      "useActiveHousehold must be used within an ActiveHouseholdProvider",
    );
  }
  return context;
}

So now, from anywhere within the client side of the application, we can leverage this hook to get access to the active household (and all its properties), the ability to set it, the list of households the user has created (authenticated users may be able to create more than one), and the loading state of the query:

const { activeHousehold, setActiveHousehold, households, isLoading } =
  useActiveHousehold();

And this is the lions share of what needed to happen. With these changes, I now have a framework for building out new features and having them operate differently based on whether or not a user is authenticated, all while keeping types coupled, which also makes testing MUCH easier than testing against varied data shapes.

See you again soon with the next update!