diff --git a/frontend/components/Domain/Recipe/RecipeCardSection.vue b/frontend/components/Domain/Recipe/RecipeCardSection.vue
index d065e0e80d6..62ca93d5431 100644
--- a/frontend/components/Domain/Recipe/RecipeCardSection.vue
+++ b/frontend/components/Domain/Recipe/RecipeCardSection.vue
@@ -69,50 +69,52 @@
         @toggle-dense-view="toggleMobileCards()"
       />
     </v-app-bar>
-    <div v-if="recipes" class="mt-2">
-      <v-row v-if="!useMobileCards">
-        <v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
-          <v-lazy>
-            <RecipeCard
-              :name="recipe.name"
-              :description="recipe.description"
-              :slug="recipe.slug"
-              :rating="recipe.rating"
-              :image="recipe.image"
-              :tags="recipe.tags"
-              :recipe-id="recipe.id"
-            />
-          </v-lazy>
-        </v-col>
-      </v-row>
-      <v-row v-else dense>
-        <v-col
-          v-for="recipe in recipes"
-          :key="recipe.name"
-          cols="12"
-          :sm="singleColumn ? '12' : '12'"
-          :md="singleColumn ? '12' : '6'"
-          :lg="singleColumn ? '12' : '4'"
-          :xl="singleColumn ? '12' : '3'"
-        >
-          <v-lazy>
-            <RecipeCardMobile
-              :name="recipe.name"
-              :description="recipe.description"
-              :slug="recipe.slug"
-              :rating="recipe.rating"
-              :image="recipe.image"
-              :tags="recipe.tags"
-              :recipe-id="recipe.id"
-            />
-          </v-lazy>
-        </v-col>
-      </v-row>
+    <div v-if="recipes && ready">
+      <div class="mt-2">
+        <v-row v-if="!useMobileCards">
+          <v-col v-for="(recipe, index) in recipes" :key="recipe.slug + index" :sm="6" :md="6" :lg="4" :xl="3">
+            <v-lazy>
+              <RecipeCard
+                :name="recipe.name"
+                :description="recipe.description"
+                :slug="recipe.slug"
+                :rating="recipe.rating"
+                :image="recipe.image"
+                :tags="recipe.tags"
+                :recipe-id="recipe.id"
+              />
+            </v-lazy>
+          </v-col>
+        </v-row>
+        <v-row v-else dense>
+          <v-col
+            v-for="recipe in recipes"
+            :key="recipe.name"
+            cols="12"
+            :sm="singleColumn ? '12' : '12'"
+            :md="singleColumn ? '12' : '6'"
+            :lg="singleColumn ? '12' : '4'"
+            :xl="singleColumn ? '12' : '3'"
+          >
+            <v-lazy>
+              <RecipeCardMobile
+                :name="recipe.name"
+                :description="recipe.description"
+                :slug="recipe.slug"
+                :rating="recipe.rating"
+                :image="recipe.image"
+                :tags="recipe.tags"
+                :recipe-id="recipe.id"
+              />
+            </v-lazy>
+          </v-col>
+        </v-row>
+      </div>
+      <v-card v-intersect="infiniteScroll"></v-card>
+      <v-fade-transition>
+        <AppLoader v-if="loading" :loading="loading" />
+      </v-fade-transition>
     </div>
-    <v-card v-intersect="infiniteScroll"></v-card>
-    <v-fade-transition>
-      <AppLoader v-if="loading" :loading="loading" />
-    </v-fade-transition>
   </div>
 </template>
 
@@ -223,36 +225,42 @@ export default defineComponent({
 
     const queryFilter = computed(() => {
       const orderBy = props.query?.orderBy || preferences.value.orderBy;
-      return preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
+      const orderByFilter = preferences.value.filterNull && orderBy ? `${orderBy} IS NOT NULL` : null;
+
+      if (props.query.queryFilter && orderByFilter) {
+        return `(${props.query.queryFilter}) AND ${orderByFilter}`;
+      } else if (props.query.queryFilter) {
+        return props.query.queryFilter;
+      } else {
+        return orderByFilter;
+      }
     });
 
     async function fetchRecipes(pageCount = 1) {
       return await fetchMore(
         page.value,
-        // we double-up the first call to avoid a bug with large screens that render the entire first page without scrolling, preventing additional loading
         perPage * pageCount,
         props.query?.orderBy || preferences.value.orderBy,
         props.query?.orderDirection || preferences.value.orderDirection,
         props.query,
-        // filter out recipes that have a null value for the property we're sorting by
+        // we use a computed queryFilter to filter out recipes that have a null value for the property we're sorting by
         queryFilter.value
       );
     }
 
     onMounted(async () => {
-      if (props.query) {
-        await initRecipes();
-        ready.value = true;
-      }
+      await initRecipes();
+      ready.value = true;
     });
 
-    let lastQuery: string | undefined;
+    let lastQuery: string | undefined = JSON.stringify(props.query);
     watch(
       () => props.query,
       async (newValue: RecipeSearchQuery | undefined) => {
         const newValueString = JSON.stringify(newValue)
-        if (newValue && (!ready.value || lastQuery !== newValueString)) {
+        if (lastQuery !== newValueString) {
           lastQuery = newValueString;
+          ready.value = false;
           await initRecipes();
           ready.value = true;
         }
@@ -261,8 +269,12 @@ export default defineComponent({
 
     async function initRecipes() {
       page.value = 1;
-      const newRecipes = await fetchRecipes(2);
-      if (!newRecipes.length) {
+      hasMore.value = true;
+
+      // we double-up the first call to avoid a bug with large screens that render
+      // the entire first page without scrolling, preventing additional loading
+      const newRecipes = await fetchRecipes(page.value + 1);
+      if (newRecipes.length < perPage) {
         hasMore.value = false;
       }
 
@@ -274,7 +286,7 @@ export default defineComponent({
 
     const infiniteScroll = useThrottleFn(() => {
       useAsync(async () => {
-        if (!ready.value || !hasMore.value || loading.value) {
+        if (!hasMore.value || loading.value) {
           return;
         }
 
@@ -282,9 +294,10 @@ export default defineComponent({
         page.value = page.value + 1;
 
         const newRecipes = await fetchRecipes();
-        if (!newRecipes.length) {
+        if (newRecipes.length < perPage) {
           hasMore.value = false;
-        } else {
+        }
+        if (newRecipes.length) {
           context.emit(APPEND_RECIPES_EVENT, newRecipes);
         }
 
@@ -379,6 +392,7 @@ export default defineComponent({
       displayTitleIcon,
       EVENTS,
       infiniteScroll,
+      ready,
       loading,
       navigateRandom,
       preferences,
diff --git a/frontend/components/Domain/Recipe/RecipeDataTable.vue b/frontend/components/Domain/Recipe/RecipeDataTable.vue
index fd8900004ce..152b08d06c6 100644
--- a/frontend/components/Domain/Recipe/RecipeDataTable.vue
+++ b/frontend/components/Domain/Recipe/RecipeDataTable.vue
@@ -3,6 +3,8 @@
     v-model="selected"
     item-key="id"
     show-select
+    sort-by="dateAdded"
+    sort-desc
     :headers="headers"
     :items="recipes"
     :items-per-page="15"
@@ -39,6 +41,9 @@
         </v-list-item-content>
       </v-list-item>
     </template>
+    <template #item.dateAdded="{ item }">
+      {{ formatDate(item.dateAdded) }}
+    </template>
   </v-data-table>
 </template>
 
@@ -132,6 +137,14 @@ export default defineComponent({
       return hdrs;
     });
 
+    function formatDate(date: string) {
+      try {
+        return i18n.d(Date.parse(date), "medium");
+      } catch {
+        return "";
+      }
+    }
+
     // ============
     // Group Members
     const api = useUserApi();
@@ -160,6 +173,7 @@ export default defineComponent({
       groupSlug,
       setValue,
       headers,
+      formatDate,
       members,
       getMember,
     };
diff --git a/frontend/components/Domain/Recipe/RecipeExplorerPage.vue b/frontend/components/Domain/Recipe/RecipeExplorerPage.vue
index 03cb0f7f213..a9c212dff57 100644
--- a/frontend/components/Domain/Recipe/RecipeExplorerPage.vue
+++ b/frontend/components/Domain/Recipe/RecipeExplorerPage.vue
@@ -53,6 +53,14 @@
             {{ $t("general.foods") }}
           </SearchFilter>
 
+          <!-- Household Filter -->
+          <SearchFilter v-if="households.length > 1" v-model="selectedHouseholds" :items="households" radio>
+            <v-icon left>
+              {{ $globals.icons.household }}
+            </v-icon>
+            {{ $t("household.households") }}
+          </SearchFilter>
+
           <!-- Sort Options -->
           <v-menu offset-y nudge-bottom="3">
             <template #activator="{ on, attrs }">
@@ -142,17 +150,25 @@ import { ref, defineComponent, useRouter, onMounted, useContext, computed, Ref,
 import { watchDebounced } from "@vueuse/shared";
 import SearchFilter from "~/components/Domain/SearchFilter.vue";
 import { useLoggedInState } from "~/composables/use-logged-in-state";
-import { useCategoryStore, useFoodStore, useTagStore, useToolStore } from "~/composables/store";
+import {
+  useCategoryStore,
+  usePublicCategoryStore,
+  useFoodStore,
+  usePublicFoodStore,
+  useHouseholdStore,
+  usePublicHouseholdStore,
+  useTagStore,
+  usePublicTagStore,
+  useToolStore,
+  usePublicToolStore,
+} from "~/composables/store";
 import { useUserSearchQuerySession } from "~/composables/use-users/preferences";
 import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
 import { IngredientFood, RecipeCategory, RecipeTag, RecipeTool } from "~/lib/api/types/recipe";
 import { NoUndefinedField } from "~/lib/api/types/non-generated";
 import { useLazyRecipes } from "~/composables/recipes";
 import { RecipeSearchQuery } from "~/lib/api/user/recipes/recipe";
-import { usePublicCategoryStore } from "~/composables/store/use-category-store";
-import { usePublicFoodStore } from "~/composables/store/use-food-store";
-import { usePublicTagStore } from "~/composables/store/use-tag-store";
-import { usePublicToolStore } from "~/composables/store/use-tool-store";
+import { HouseholdSummary } from "~/lib/api/types/household";
 
 export default defineComponent({
   components: { SearchFilter, RecipeCardSection },
@@ -186,6 +202,9 @@ export default defineComponent({
     const foods = isOwnGroup.value ? useFoodStore() : usePublicFoodStore(groupSlug.value);
     const selectedFoods = ref<IngredientFood[]>([]);
 
+    const households = isOwnGroup.value ? useHouseholdStore() : usePublicHouseholdStore(groupSlug.value);
+    const selectedHouseholds = ref([] as NoUndefinedField<HouseholdSummary>[]);
+
     const tags = isOwnGroup.value ? useTagStore() : usePublicTagStore(groupSlug.value);
     const selectedTags = ref<NoUndefinedField<RecipeTag>[]>([]);
 
@@ -199,6 +218,7 @@ export default defineComponent({
         search: state.value.search ? state.value.search : "",
         categories: toIDArray(selectedCategories.value),
         foods: toIDArray(selectedFoods.value),
+        households: toIDArray(selectedHouseholds.value),
         tags: toIDArray(selectedTags.value),
         tools: toIDArray(selectedTools.value),
         requireAllCategories: state.value.requireAllCategories,
@@ -239,10 +259,9 @@ export default defineComponent({
       state.value.requireAllFoods = queryDefaults.requireAllFoods;
       selectedCategories.value = [];
       selectedFoods.value = [];
+      selectedHouseholds.value = [];
       selectedTags.value = [];
       selectedTools.value = [];
-
-      search();
     }
 
     function toggleOrderDirection() {
@@ -280,6 +299,7 @@ export default defineComponent({
           search: passedQuery.value.search === queryDefaults.search ? undefined : passedQuery.value.search,
           orderBy: passedQuery.value.orderBy === queryDefaults.orderBy ? undefined : passedQuery.value.orderBy,
           orderDirection: passedQuery.value.orderDirection === queryDefaults.orderDirection ? undefined : passedQuery.value.orderDirection,
+          households: !passedQuery.value.households?.length || passedQuery.value.households?.length === households.store.value.length ? undefined : passedQuery.value.households,
           requireAllCategories: passedQuery.value.requireAllCategories ? "true" : undefined,
           requireAllTags: passedQuery.value.requireAllTags ? "true" : undefined,
           requireAllTools: passedQuery.value.requireAllTools ? "true" : undefined,
@@ -361,13 +381,10 @@ export default defineComponent({
     watch(
       () => route.value.query,
       () => {
-        if (state.value.ready) {
-          hydrateSearch();
+        if (!Object.keys(route.value.query).length) {
+          reset();
         }
-      },
-      {
-        deep: true,
-      },
+      }
     )
 
     async function hydrateSearch() {
@@ -423,9 +440,9 @@ export default defineComponent({
       if (query.categories?.length) {
         promises.push(
           waitUntilAndExecute(
-            () => categories.items.value.length > 0,
+            () => categories.store.value.length > 0,
             () => {
-              const result = categories.items.value.filter((item) =>
+              const result = categories.store.value.filter((item) =>
                 (query.categories as string[]).includes(item.id as string)
               );
 
@@ -440,9 +457,9 @@ export default defineComponent({
       if (query.tags?.length) {
         promises.push(
           waitUntilAndExecute(
-            () => tags.items.value.length > 0,
+            () => tags.store.value.length > 0,
             () => {
-              const result = tags.items.value.filter((item) => (query.tags as string[]).includes(item.id as string));
+              const result = tags.store.value.filter((item) => (query.tags as string[]).includes(item.id as string));
               selectedTags.value = result as NoUndefinedField<RecipeTag>[];
             }
           )
@@ -454,9 +471,9 @@ export default defineComponent({
       if (query.tools?.length) {
         promises.push(
           waitUntilAndExecute(
-            () => tools.items.value.length > 0,
+            () => tools.store.value.length > 0,
             () => {
-              const result = tools.items.value.filter((item) => (query.tools as string[]).includes(item.id));
+              const result = tools.store.value.filter((item) => (query.tools as string[]).includes(item.id));
               selectedTools.value = result as NoUndefinedField<RecipeTool>[];
             }
           )
@@ -469,13 +486,13 @@ export default defineComponent({
         promises.push(
           waitUntilAndExecute(
             () => {
-              if (foods.foods.value) {
-                return foods.foods.value.length > 0;
+              if (foods.store.value) {
+                return foods.store.value.length > 0;
               }
               return false;
             },
             () => {
-              const result = foods.foods.value?.filter((item) => (query.foods as string[]).includes(item.id));
+              const result = foods.store.value?.filter((item) => (query.foods as string[]).includes(item.id));
               selectedFoods.value = result ?? [];
             }
           )
@@ -484,6 +501,25 @@ export default defineComponent({
         selectedFoods.value = [];
       }
 
+      if (query.households?.length) {
+        promises.push(
+          waitUntilAndExecute(
+            () => {
+              if (households.store.value) {
+                return households.store.value.length > 0;
+              }
+              return false;
+            },
+            () => {
+              const result = households.store.value?.filter((item) => (query.households as string[]).includes(item.id));
+              selectedHouseholds.value = result as NoUndefinedField<HouseholdSummary>[] ?? [];
+            }
+          )
+        );
+      } else {
+        selectedHouseholds.value = [];
+      }
+
       await Promise.allSettled(promises);
     };
 
@@ -515,6 +551,7 @@ export default defineComponent({
         () => state.value.orderDirection,
         selectedCategories,
         selectedFoods,
+        selectedHouseholds,
         selectedTags,
         selectedTools,
       ],
@@ -533,10 +570,11 @@ export default defineComponent({
       search,
       reset,
       state,
-      categories: categories.items as unknown as NoUndefinedField<RecipeCategory>[],
-      tags: tags.items as unknown as NoUndefinedField<RecipeTag>[],
-      foods: foods.foods,
-      tools: tools.items as unknown as NoUndefinedField<RecipeTool>[],
+      categories: categories.store as unknown as NoUndefinedField<RecipeCategory>[],
+      tags: tags.store as unknown as NoUndefinedField<RecipeTag>[],
+      foods: foods.store,
+      tools: tools.store as unknown as NoUndefinedField<RecipeTool>[],
+      households: households.store as unknown as NoUndefinedField<HouseholdSummary>[],
 
       sortable,
       toggleOrderDirection,
@@ -545,6 +583,7 @@ export default defineComponent({
 
       selectedCategories,
       selectedFoods,
+      selectedHouseholds,
       selectedTags,
       selectedTools,
       appendRecipes,
diff --git a/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue b/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue
index f1e8e54f830..cdf37107897 100644
--- a/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue
+++ b/frontend/components/Domain/Recipe/RecipeIngredientEditor.vue
@@ -289,11 +289,11 @@ export default defineComponent({
       createAssignFood,
       unitAutocomplete,
       createAssignUnit,
-      foods: foodStore.foods,
+      foods: foodStore.store,
       foodSearch,
       toggleTitle,
       unitActions: unitStore.actions,
-      units: unitStore.units,
+      units: unitStore.store,
       unitSearch,
       validators,
       workingUnitData: unitsData.data,
diff --git a/frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue b/frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
index 378cda2b061..91f4478868d 100644
--- a/frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
+++ b/frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
@@ -135,7 +135,7 @@ export default defineComponent({
         await store.actions.createOne({ ...state });
       }
 
-      const newItem = store.items.value.find((item) => item.name === state.name);
+      const newItem = store.store.value.find((item) => item.name === state.name);
 
       context.emit(CREATED_ITEM_EVENT, newItem);
       dialog.value = false;
diff --git a/frontend/components/Domain/Recipe/RecipeOrganizerSelector.vue b/frontend/components/Domain/Recipe/RecipeOrganizerSelector.vue
index 16fd636bf24..658f043574b 100644
--- a/frontend/components/Domain/Recipe/RecipeOrganizerSelector.vue
+++ b/frontend/components/Domain/Recipe/RecipeOrganizerSelector.vue
@@ -127,9 +127,9 @@ export default defineComponent({
 
     const items = computed(() => {
       if (!props.returnObject) {
-        return store.items.value.map((item) => item.name);
+        return store.store.value.map((item) => item.name);
       }
-      return store.items.value;
+      return store.store.value;
     });
 
     function removeByIndex(index: number) {
diff --git a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
index 69853821a79..0fa48f2b0ec 100644
--- a/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
+++ b/frontend/components/Domain/Recipe/RecipePage/RecipePageParts/RecipePageHeader.vue
@@ -105,7 +105,7 @@ export default defineComponent({
     const recipeHousehold = ref<HouseholdSummary>();
     if (user) {
       const userApi = useUserApi();
-      userApi.groups.fetchHousehold(props.recipe.householdId).then(({ data }) => {
+      userApi.households.getOne(props.recipe.householdId).then(({ data }) => {
         recipeHousehold.value = data || undefined;
       });
     }
diff --git a/frontend/components/Domain/SearchFilter.vue b/frontend/components/Domain/SearchFilter.vue
index 44e0ef05509..bdacf3aa6e2 100644
--- a/frontend/components/Domain/SearchFilter.vue
+++ b/frontend/components/Domain/SearchFilter.vue
@@ -11,28 +11,43 @@
       <v-card width="400">
         <v-card-text>
           <v-text-field v-model="state.search" class="mb-2" hide-details dense :label="$tc('search.search')" clearable />
-          <v-switch
-            v-if="requireAll != undefined"
-            v-model="requireAllValue"
-            dense
-            small
-            :label="`${requireAll ? $tc('search.has-all') : $tc('search.has-any')}`"
-          >
-          </v-switch>
+          <div class="d-flex py-4">
+            <v-switch
+              v-if="requireAll != undefined"
+              v-model="requireAllValue"
+              dense
+              small
+              hide-details
+              class="my-auto"
+              :label="`${requireAll ? $tc('search.has-all') : $tc('search.has-any')}`"
+            />
+            <v-spacer />
+            <v-btn
+              small
+              color="accent"
+              class="mr-2 my-auto"
+              @click="clearSelection"
+            >
+              {{ $tc("search.clear-selection") }}
+            </v-btn>
+          </div>
           <v-card v-if="filtered.length > 0" flat outlined>
+            <v-radio-group v-model="selectedRadio" class="ma-0 pa-0">
             <v-virtual-scroll :items="filtered" height="300" item-height="51">
               <template #default="{ item }">
-                <v-list-item :key="item.id" dense :value="item">
-                  <v-list-item-action>
-                    <v-checkbox v-model="selected" :value="item"></v-checkbox>
-                  </v-list-item-action>
-                  <v-list-item-content>
-                    <v-list-item-title> {{ item.name }}</v-list-item-title>
-                  </v-list-item-content>
-                </v-list-item>
+                  <v-list-item :key="item.id" dense :value="item">
+                    <v-list-item-action>
+                      <v-radio v-if="radio" :value="item" @click="handleRadioClick(item)" />
+                      <v-checkbox v-else v-model="selected" :value="item" />
+                    </v-list-item-action>
+                    <v-list-item-content>
+                      <v-list-item-title> {{ item.name }} </v-list-item-title>
+                    </v-list-item-content>
+                  </v-list-item>
                 <v-divider></v-divider>
               </template>
             </v-virtual-scroll>
+            </v-radio-group>
           </v-card>
           <div v-else>
             <v-alert type="info" text> {{ $tc('search.no-results') }} </v-alert>
@@ -65,6 +80,10 @@ export default defineComponent({
       type: Boolean,
       default: undefined,
     },
+    radio: {
+      type: Boolean,
+      default: false,
+    },
   },
   setup(props, context) {
     const state = reactive({
@@ -86,6 +105,13 @@ export default defineComponent({
       },
     });
 
+    const selectedRadio = computed({
+      get: () => (selected.value.length > 0 ? selected.value[0] : null),
+      set: (value) => {
+        context.emit("input", value ? [value] : []);
+      },
+    });
+
     const filtered = computed(() => {
       if (!state.search) {
         return props.items;
@@ -94,11 +120,26 @@ export default defineComponent({
       return props.items.filter((item) => item.name.toLowerCase().includes(state.search.toLowerCase()));
     });
 
+    const handleRadioClick = (item: SelectableItem) => {
+      if (selectedRadio.value === item) {
+        selectedRadio.value = null;
+      }
+    };
+
+    function clearSelection() {
+      selected.value = [];
+      selectedRadio.value = null;
+      state.search = "";
+    }
+
     return {
       requireAllValue,
       state,
       selected,
+      selectedRadio,
       filtered,
+      handleRadioClick,
+      clearSelection,
     };
   },
 });
diff --git a/frontend/components/global/CrudTable.vue b/frontend/components/global/CrudTable.vue
index caf25a803a2..1c7bfe3c9c2 100644
--- a/frontend/components/global/CrudTable.vue
+++ b/frontend/components/global/CrudTable.vue
@@ -44,6 +44,8 @@
       item-key="id"
       :show-select="bulkActions.length > 0"
       :headers="activeHeaders"
+      :sort-by="initialSort"
+      :sort-desc="initialSortDesc"
       :items="data || []"
       :items-per-page="15"
       :search="search"
@@ -126,6 +128,14 @@ export default defineComponent({
       type: Array as () => BulkAction[],
       default: () => [],
     },
+    initialSort: {
+      type: String,
+      default: "id",
+    },
+    initialSortDesc: {
+      type: Boolean,
+      default: false,
+    },
   },
   setup(props, context) {
     // ===========================================================
diff --git a/frontend/composables/api/index.ts b/frontend/composables/api/index.ts
index 3f9056368b8..20d74981d77 100644
--- a/frontend/composables/api/index.ts
+++ b/frontend/composables/api/index.ts
@@ -1,3 +1,3 @@
 export { useAppInfo } from "./use-app-info";
 export { useStaticRoutes } from "./static-routes";
-export { useAdminApi, useUserApi } from "./api-client";
+export { useAdminApi, usePublicApi, usePublicExploreApi, useUserApi } from "./api-client";
diff --git a/frontend/composables/partials/types.ts b/frontend/composables/partials/types.ts
new file mode 100644
index 00000000000..1be933e3e77
--- /dev/null
+++ b/frontend/composables/partials/types.ts
@@ -0,0 +1,3 @@
+export type BoundT = {
+  id?: string | number | null;
+};
diff --git a/frontend/composables/partials/use-actions-factory.ts b/frontend/composables/partials/use-actions-factory.ts
index f0b829c78ba..b17b9ac6a3b 100644
--- a/frontend/composables/partials/use-actions-factory.ts
+++ b/frontend/composables/partials/use-actions-factory.ts
@@ -1,18 +1,15 @@
 import { Ref, useAsync } from "@nuxtjs/composition-api";
 import { useAsyncKey } from "../use-utils";
+import { BoundT } from "./types";
 import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
 import { QueryValue } from "~/lib/api/base/route";
 
-type BoundT = {
-  id?: string | number | null;
-};
-
-interface PublicStoreActions<T extends BoundT> {
+interface ReadOnlyStoreActions<T extends BoundT> {
   getAll(page?: number, perPage?: number, params?: any): Ref<T[] | null>;
   refresh(): Promise<void>;
 }
 
-interface StoreActions<T extends BoundT> extends PublicStoreActions<T> {
+interface StoreActions<T extends BoundT> extends ReadOnlyStoreActions<T> {
   createOne(createData: T): Promise<T | null>;
   updateOne(updateData: T): Promise<T | null>;
   deleteOne(id: string | number): Promise<T | null>;
@@ -20,16 +17,16 @@ interface StoreActions<T extends BoundT> extends PublicStoreActions<T> {
 
 
 /**
- * usePublicStoreActions is a factory function that returns a set of methods
+ * useReadOnlyActions is a factory function that returns a set of methods
  * that can be reused to manage the state of a data store without using
  * Vuex. This is primarily used for basic GET/GETALL operations that required
  * a lot of refreshing hooks to be called on operations
  */
-export function usePublicStoreActions<T extends BoundT>(
+export function useReadOnlyActions<T extends BoundT>(
   api: BaseCRUDAPIReadOnly<T>,
   allRef: Ref<T[] | null> | null,
   loading: Ref<boolean>
-): PublicStoreActions<T> {
+): ReadOnlyStoreActions<T> {
   function getAll(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
     params.orderBy ??= "name";
     params.orderDirection ??= "asc";
diff --git a/frontend/composables/partials/use-store-factory.ts b/frontend/composables/partials/use-store-factory.ts
new file mode 100644
index 00000000000..f4f80e14588
--- /dev/null
+++ b/frontend/composables/partials/use-store-factory.ts
@@ -0,0 +1,53 @@
+import { ref, reactive, Ref } from "@nuxtjs/composition-api";
+import { useReadOnlyActions, useStoreActions } from "./use-actions-factory";
+import { BoundT } from "./types";
+import { BaseCRUDAPI, BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
+
+export const useData = function<T extends BoundT>(defaultObject: T) {
+  const data = reactive({ ...defaultObject });
+  function reset() {
+    Object.assign(data, defaultObject);
+  };
+
+  return { data, reset };
+}
+
+export const useReadOnlyStore = function<T extends BoundT>(
+  store: Ref<T[]>,
+  loading: Ref<boolean>,
+  api: BaseCRUDAPIReadOnly<T>,
+) {
+  const actions = {
+    ...useReadOnlyActions(api, store, loading),
+    flushStore() {
+      store.value = [];
+    },
+  };
+
+  if (!loading.value && (!store.value || store.value.length === 0)) {
+    const result = actions.getAll();
+    store.value = result.value || [];
+  }
+
+  return { store, actions };
+}
+
+export const useStore = function<T extends BoundT>(
+  store: Ref<T[]>,
+  loading: Ref<boolean>,
+  api: BaseCRUDAPI<unknown, T, unknown>,
+) {
+  const actions = {
+    ...useStoreActions(api, store, loading),
+    flushStore() {
+      store = ref([]);
+    },
+  };
+
+  if (!loading.value && (!store.value || store.value.length === 0)) {
+    const result = actions.getAll();
+    store.value = result.value || [];
+  }
+
+  return { store, actions };
+}
diff --git a/frontend/composables/recipes/use-recipes.ts b/frontend/composables/recipes/use-recipes.ts
index 14a6fbcfd1b..c51149a29a8 100644
--- a/frontend/composables/recipes/use-recipes.ts
+++ b/frontend/composables/recipes/use-recipes.ts
@@ -32,6 +32,7 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
       searchSeed: query?._searchSeed, // unused, but pass it along for completeness of data
       search: query?.search,
       cookbook: query?.cookbook,
+      households: query?.households,
       categories: query?.categories,
       requireAllCategories: query?.requireAllCategories,
       tags: query?.tags,
diff --git a/frontend/composables/store/index.ts b/frontend/composables/store/index.ts
index e00aba57e1a..9dad0b7f161 100644
--- a/frontend/composables/store/index.ts
+++ b/frontend/composables/store/index.ts
@@ -1,6 +1,7 @@
-export { useFoodStore, useFoodData } from "./use-food-store";
-export { useUnitStore, useUnitData } from "./use-unit-store";
+export { useCategoryStore, usePublicCategoryStore, useCategoryData } from "./use-category-store";
+export { useFoodStore, usePublicFoodStore, useFoodData } from "./use-food-store";
+export { useHouseholdStore, usePublicHouseholdStore } from "./use-household-store";
 export { useLabelStore, useLabelData } from "./use-label-store";
-export { useToolStore, useToolData } from "./use-tool-store";
-export { useCategoryStore, useCategoryData } from "./use-category-store";
-export { useTagStore, useTagData } from "./use-tag-store";
+export { useTagStore, usePublicTagStore, useTagData } from "./use-tag-store";
+export { useToolStore, usePublicToolStore, useToolData } from "./use-tool-store";
+export { useUnitStore, useUnitData } from "./use-unit-store";
diff --git a/frontend/composables/store/use-category-store.ts b/frontend/composables/store/use-category-store.ts
index 4801bc9ab4b..e64cd060a59 100644
--- a/frontend/composables/store/use-category-store.ts
+++ b/frontend/composables/store/use-category-store.ts
@@ -1,73 +1,26 @@
-import { reactive, ref, Ref } from "@nuxtjs/composition-api";
-import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
-import { usePublicExploreApi } from "../api/api-client";
-import { useUserApi } from "~/composables/api";
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
 import { RecipeCategory } from "~/lib/api/types/recipe";
+import { usePublicExploreApi, useUserApi } from "~/composables/api";
 
-const categoryStore: Ref<RecipeCategory[]> = ref([]);
-const publicStoreLoading = ref(false);
-const storeLoading = ref(false);
+const store: Ref<RecipeCategory[]> = ref([]);
+const loading = ref(false);
+const publicLoading = ref(false);
 
-export function useCategoryData() {
-  const data = reactive({
+export const useCategoryData = function () {
+  return useData<RecipeCategory>({
     id: "",
     name: "",
-    slug: undefined,
+    slug: "",
   });
-
-  function reset() {
-    data.id = "";
-    data.name = "";
-    data.slug = undefined;
-  }
-
-  return {
-    data,
-    reset,
-  };
-}
-
-export function usePublicCategoryStore(groupSlug: string) {
-  const api = usePublicExploreApi(groupSlug).explore;
-  const loading = publicStoreLoading;
-
-  const actions = {
-    ...usePublicStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
-    flushStore() {
-      categoryStore.value = [];
-    },
-  };
-
-  if (!loading.value && (!categoryStore.value || categoryStore.value?.length === 0)) {
-    actions.getAll();
-  }
-
-  return {
-    items: categoryStore,
-    actions,
-    loading,
-  };
 }
 
-export function useCategoryStore() {
-  // passing the group slug switches to using the public API
+export const useCategoryStore = function () {
   const api = useUserApi();
-  const loading = storeLoading;
-
-  const actions = {
-    ...useStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
-    flushStore() {
-      categoryStore.value = [];
-    },
-  };
-
-  if (!loading.value && (!categoryStore.value || categoryStore.value?.length === 0)) {
-    actions.getAll();
-  }
+  return useStore<RecipeCategory>(store, loading, api.categories);
+}
 
-  return {
-    items: categoryStore,
-    actions,
-    loading,
-  };
+export const usePublicCategoryStore = function (groupSlug: string) {
+  const api = usePublicExploreApi(groupSlug).explore;
+  return useReadOnlyStore<RecipeCategory>(store, publicLoading, api.categories);
 }
diff --git a/frontend/composables/store/use-food-store.ts b/frontend/composables/store/use-food-store.ts
index 4b02210c382..f377763fea3 100644
--- a/frontend/composables/store/use-food-store.ts
+++ b/frontend/composables/store/use-food-store.ts
@@ -1,73 +1,28 @@
-import { ref, reactive, Ref } from "@nuxtjs/composition-api";
-import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
-import { usePublicExploreApi } from "../api/api-client";
-import { useUserApi } from "~/composables/api";
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
 import { IngredientFood } from "~/lib/api/types/recipe";
+import { usePublicExploreApi, useUserApi } from "~/composables/api";
 
-let foodStore: Ref<IngredientFood[] | null> = ref([]);
-const publicStoreLoading = ref(false);
-const storeLoading = ref(false);
+const store: Ref<IngredientFood[]> = ref([]);
+const loading = ref(false);
+const publicLoading = ref(false);
 
-/**
- * useFoodData returns a template reactive object
- * for managing the creation of foods. It also provides a
- * function to reset the data back to the initial state.
- */
 export const useFoodData = function () {
-  const data: IngredientFood = reactive({
+  return useData<IngredientFood>({
     id: "",
     name: "",
     description: "",
     labelId: undefined,
     onHand: false,
   });
-
-  function reset() {
-    data.id = "";
-    data.name = "";
-    data.description = "";
-    data.labelId = undefined;
-    data.onHand = false;
-  }
-
-  return {
-    data,
-    reset,
-  };
-};
-
-export const usePublicFoodStore = function (groupSlug: string) {
-  const api = usePublicExploreApi(groupSlug).explore;
-  const loading = publicStoreLoading;
-
-  const actions = {
-    ...usePublicStoreActions(api.foods, foodStore, loading),
-    flushStore() {
-      foodStore = ref([]);
-    },
-  };
-
-  if (!loading.value && (!foodStore.value || foodStore.value.length === 0)) {
-    foodStore = actions.getAll();
-  }
-
-  return { foods: foodStore, actions };
-};
+}
 
 export const useFoodStore = function () {
   const api = useUserApi();
-  const loading = storeLoading;
-
-  const actions = {
-    ...useStoreActions(api.foods, foodStore, loading),
-    flushStore() {
-      foodStore.value = [];
-    },
-  };
+  return useStore<IngredientFood>(store, loading, api.foods);
+}
 
-  if (!loading.value && (!foodStore.value || foodStore.value.length === 0)) {
-    foodStore = actions.getAll();
-  }
-
-  return { foods: foodStore, actions };
-};
+export const usePublicFoodStore = function (groupSlug: string) {
+  const api = usePublicExploreApi(groupSlug).explore;
+  return useReadOnlyStore<IngredientFood>(store, publicLoading, api.foods);
+}
diff --git a/frontend/composables/store/use-household-store.ts b/frontend/composables/store/use-household-store.ts
new file mode 100644
index 00000000000..0b7c8eef102
--- /dev/null
+++ b/frontend/composables/store/use-household-store.ts
@@ -0,0 +1,18 @@
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useReadOnlyStore } from "../partials/use-store-factory";
+import { HouseholdSummary } from "~/lib/api/types/household";
+import { usePublicExploreApi, useUserApi } from "~/composables/api";
+
+const store: Ref<HouseholdSummary[]> = ref([]);
+const loading = ref(false);
+const publicLoading = ref(false);
+
+export const useHouseholdStore = function () {
+  const api = useUserApi();
+  return useReadOnlyStore<HouseholdSummary>(store, loading, api.households);
+}
+
+export const usePublicHouseholdStore = function (groupSlug: string) {
+  const api = usePublicExploreApi(groupSlug).explore;
+  return useReadOnlyStore<HouseholdSummary>(store, publicLoading, api.households);
+}
diff --git a/frontend/composables/store/use-label-store.ts b/frontend/composables/store/use-label-store.ts
index 72654d3b63a..0cd3bb58d9d 100644
--- a/frontend/composables/store/use-label-store.ts
+++ b/frontend/composables/store/use-label-store.ts
@@ -1,50 +1,21 @@
-import { reactive, ref, Ref } from "@nuxtjs/composition-api";
-import { useStoreActions } from "../partials/use-actions-factory";
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useData, useStore } from "../partials/use-store-factory";
 import { MultiPurposeLabelOut } from "~/lib/api/types/labels";
 import { useUserApi } from "~/composables/api";
 
-let labelStore: Ref<MultiPurposeLabelOut[] | null> = ref([]);
-const storeLoading = ref(false);
+const store: Ref<MultiPurposeLabelOut[]> = ref([]);
+const loading = ref(false);
 
-export function useLabelData() {
-  const data = reactive({
+export const useLabelData = function () {
+  return useData<MultiPurposeLabelOut>({
     groupId: "",
     id: "",
     name: "",
     color: "",
   });
-
-  function reset() {
-    data.groupId = "";
-    data.id = "";
-    data.name = "";
-    data.color = "";
-  }
-
-  return {
-    data,
-    reset,
-  };
 }
 
-export function useLabelStore() {
+export const useLabelStore = function () {
   const api = useUserApi();
-  const loading = storeLoading;
-
-  const actions = {
-    ...useStoreActions<MultiPurposeLabelOut>(api.multiPurposeLabels, labelStore, loading),
-    flushStore() {
-      labelStore.value = [];
-    },
-  };
-
-  if (!loading.value && (!labelStore.value || labelStore.value?.length === 0)) {
-    labelStore = actions.getAll();
-  }
-
-  return {
-    labels: labelStore,
-    actions,
-    loading,
-  };
+  return useStore<MultiPurposeLabelOut>(store, loading, api.multiPurposeLabels);
 }
diff --git a/frontend/composables/store/use-tag-store.ts b/frontend/composables/store/use-tag-store.ts
index 395c8e48758..b5a30822aec 100644
--- a/frontend/composables/store/use-tag-store.ts
+++ b/frontend/composables/store/use-tag-store.ts
@@ -1,72 +1,26 @@
-import { reactive, ref, Ref } from "@nuxtjs/composition-api";
-import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
-import { usePublicExploreApi } from "../api/api-client";
-import { useUserApi } from "~/composables/api";
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
 import { RecipeTag } from "~/lib/api/types/recipe";
+import { usePublicExploreApi, useUserApi } from "~/composables/api";
 
-const items: Ref<RecipeTag[]> = ref([]);
-const publicStoreLoading = ref(false);
-const storeLoading = ref(false);
+const store: Ref<RecipeTag[]> = ref([]);
+const loading = ref(false);
+const publicLoading = ref(false);
 
-export function useTagData() {
-  const data = reactive({
+export const useTagData = function () {
+  return useData<RecipeTag>({
     id: "",
     name: "",
-    slug: undefined,
+    slug: "",
   });
-
-  function reset() {
-    data.id = "";
-    data.name = "";
-    data.slug = undefined;
-  }
-
-  return {
-    data,
-    reset,
-  };
-}
-
-export function usePublicTagStore(groupSlug: string) {
-  const api = usePublicExploreApi(groupSlug).explore;
-  const loading = publicStoreLoading;
-
-  const actions = {
-    ...usePublicStoreActions<RecipeTag>(api.tags, items, loading),
-    flushStore() {
-      items.value = [];
-    },
-  };
-
-  if (!loading.value && (!items.value || items.value?.length === 0)) {
-    actions.getAll();
-  }
-
-  return {
-    items,
-    actions,
-    loading,
-  };
 }
 
-export function useTagStore() {
+export const useTagStore = function () {
   const api = useUserApi();
-  const loading = storeLoading;
-
-  const actions = {
-    ...useStoreActions<RecipeTag>(api.tags, items, loading),
-    flushStore() {
-      items.value = [];
-    },
-  };
-
-  if (!loading.value && (!items.value || items.value?.length === 0)) {
-    actions.getAll();
-  }
+  return useStore<RecipeTag>(store, loading, api.tags);
+}
 
-  return {
-    items,
-    actions,
-    loading,
-  };
+export const usePublicTagStore = function (groupSlug: string) {
+  const api = usePublicExploreApi(groupSlug).explore;
+  return useReadOnlyStore<RecipeTag>(store, publicLoading, api.tags);
 }
diff --git a/frontend/composables/store/use-tool-store.ts b/frontend/composables/store/use-tool-store.ts
index 7b14381e5bb..d27fa20c089 100644
--- a/frontend/composables/store/use-tool-store.ts
+++ b/frontend/composables/store/use-tool-store.ts
@@ -1,74 +1,27 @@
-import { reactive, ref, Ref } from "@nuxtjs/composition-api";
-import { usePublicExploreApi } from "../api/api-client";
-import { usePublicStoreActions, useStoreActions } from "../partials/use-actions-factory";
-import { useUserApi } from "~/composables/api";
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useData, useReadOnlyStore, useStore } from "../partials/use-store-factory";
 import { RecipeTool } from "~/lib/api/types/recipe";
+import { usePublicExploreApi, useUserApi } from "~/composables/api";
 
-const toolStore: Ref<RecipeTool[]> = ref([]);
-const publicStoreLoading = ref(false);
-const storeLoading = ref(false);
+const store: Ref<RecipeTool[]> = ref([]);
+const loading = ref(false);
+const publicLoading = ref(false);
 
-export function useToolData() {
-  const data = reactive({
+export const useToolData = function () {
+  return useData<RecipeTool>({
     id: "",
     name: "",
-    slug: undefined,
+    slug: "",
     onHand: false,
   });
-
-  function reset() {
-    data.id = "";
-    data.name = "";
-    data.slug = undefined;
-    data.onHand = false;
-  }
-
-  return {
-    data,
-    reset,
-  };
-}
-
-export function usePublicToolStore(groupSlug: string) {
-  const api = usePublicExploreApi(groupSlug).explore;
-  const loading = publicStoreLoading;
-
-  const actions = {
-    ...usePublicStoreActions<RecipeTool>(api.tools, toolStore, loading),
-    flushStore() {
-      toolStore.value = [];
-    },
-  };
-
-  if (!loading.value && (!toolStore.value || toolStore.value?.length === 0)) {
-    actions.getAll();
-  }
-
-  return {
-    items: toolStore,
-    actions,
-    loading,
-  };
 }
 
-export function useToolStore() {
+export const useToolStore = function () {
   const api = useUserApi();
-  const loading = storeLoading;
-
-  const actions = {
-    ...useStoreActions<RecipeTool>(api.tools, toolStore, loading),
-    flushStore() {
-      toolStore.value = [];
-    },
-  };
-
-  if (!loading.value && (!toolStore.value || toolStore.value?.length === 0)) {
-    actions.getAll();
-  }
+  return useStore<RecipeTool>(store, loading, api.tools);
+}
 
-  return {
-    items: toolStore,
-    actions,
-    loading,
-  };
+export const usePublicToolStore = function (groupSlug: string) {
+  const api = usePublicExploreApi(groupSlug).explore;
+  return useReadOnlyStore<RecipeTool>(store, publicLoading, api.tools);
 }
diff --git a/frontend/composables/store/use-unit-store.ts b/frontend/composables/store/use-unit-store.ts
index 527a2ea7707..3bf0926a66b 100644
--- a/frontend/composables/store/use-unit-store.ts
+++ b/frontend/composables/store/use-unit-store.ts
@@ -1,53 +1,22 @@
-import { ref, reactive, Ref } from "@nuxtjs/composition-api";
-import { useStoreActions } from "../partials/use-actions-factory";
-import { useUserApi } from "~/composables/api";
+import { ref, Ref } from "@nuxtjs/composition-api";
+import { useData, useStore } from "../partials/use-store-factory";
 import { IngredientUnit } from "~/lib/api/types/recipe";
+import { useUserApi } from "~/composables/api";
 
-let unitStore: Ref<IngredientUnit[] | null> = ref([]);
-const storeLoading = ref(false);
+const store: Ref<IngredientUnit[]> = ref([]);
+const loading = ref(false);
 
-/**
- * useUnitData returns a template reactive object
- * for managing the creation of units. It also provides a
- * function to reset the data back to the initial state.
- */
 export const useUnitData = function () {
-  const data: IngredientUnit = reactive({
+  return useData<IngredientUnit>({
     id: "",
     name: "",
     fraction: true,
     abbreviation: "",
     description: "",
   });
-
-  function reset() {
-    data.id = "";
-    data.name = "";
-    data.fraction = true;
-    data.abbreviation = "";
-    data.description = "";
-  }
-
-  return {
-    data,
-    reset,
-  };
-};
+}
 
 export const useUnitStore = function () {
   const api = useUserApi();
-  const loading = storeLoading;
-
-  const actions = {
-    ...useStoreActions<IngredientUnit>(api.units, unitStore, loading),
-    flushStore() {
-      unitStore.value = [];
-    },
-  };
-
-  if (!loading.value && (!unitStore.value || unitStore.value.length === 0)) {
-    unitStore = actions.getAll();
-  }
-
-  return { units: unitStore, actions };
-};
+  return useStore<IngredientUnit>(store, loading, api.units);
+}
diff --git a/frontend/composables/use-households.ts b/frontend/composables/use-households.ts
index 44f9caf8f4e..c22d04422de 100644
--- a/frontend/composables/use-households.ts
+++ b/frontend/composables/use-households.ts
@@ -1,5 +1,5 @@
 import { computed, ref, Ref, useAsync } from "@nuxtjs/composition-api";
-import { useUserApi } from "~/composables/api";
+import { useAdminApi, useUserApi } from "~/composables/api";
 import { HouseholdCreate, HouseholdInDB } from "~/lib/api/types/household";
 
 const householdSelfRef = ref<HouseholdInDB | null>(null);
@@ -46,8 +46,8 @@ export const useHouseholdSelf = function () {
   return { actions, household };
 };
 
-export const useHouseholds = function () {
-  const api = useUserApi();
+export const useAdminHouseholds = function () {
+  const api = useAdminApi();
   const loading = ref(false);
 
   function getAllHouseholds() {
diff --git a/frontend/composables/use-users/user-ratings.ts b/frontend/composables/use-users/user-ratings.ts
index cf29576786b..0f82cd71893 100644
--- a/frontend/composables/use-users/user-ratings.ts
+++ b/frontend/composables/use-users/user-ratings.ts
@@ -7,34 +7,37 @@ const loading = ref(false);
 const ready = ref(false);
 
 export const useUserSelfRatings = function () {
-    const { $auth } = useContext();
-    const api = useUserApi();
+  const { $auth } = useContext();
+  const api = useUserApi();
 
-    async function refreshUserRatings() {
-        if (loading.value) {
-            return;
-        }
-
-        loading.value = true;
-        const { data } = await api.users.getSelfRatings();
-        userRatings.value = data?.ratings || [];
-        loading.value = false;
-        ready.value = true;
+  async function refreshUserRatings() {
+    if (!$auth.user || loading.value) {
+      return;
     }
 
-    async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
-        loading.value = true;
-        const userId = $auth.user?.id || "";
-        await api.users.setRating(userId, slug, rating, isFavorite);
-        loading.value = false;
-        await refreshUserRatings();
-    }
+    loading.value = true;
+    const { data } = await api.users.getSelfRatings();
+    userRatings.value = data?.ratings || [];
+    loading.value = false;
+    ready.value = true;
+  }
+
+  async function setRating(slug: string, rating: number | null, isFavorite: boolean | null) {
+    loading.value = true;
+    const userId = $auth.user?.id || "";
+    await api.users.setRating(userId, slug, rating, isFavorite);
+    loading.value = false;
+    await refreshUserRatings();
+  }
 
+  if (!ready.value) {
     refreshUserRatings();
-    return {
-        userRatings,
-        refreshUserRatings,
-        setRating,
-        ready,
-    }
+  }
+
+  return {
+    userRatings,
+    refreshUserRatings,
+    setRating,
+    ready,
+  }
 }
diff --git a/frontend/lang/messages/en-US.json b/frontend/lang/messages/en-US.json
index 382aa53aca0..2dd9dcee3c2 100644
--- a/frontend/lang/messages/en-US.json
+++ b/frontend/lang/messages/en-US.json
@@ -652,6 +652,7 @@
     "or": "Or",
     "has-any": "Has Any",
     "has-all": "Has All",
+    "clear-selection": "Clear Selection",
     "results": "Results",
     "search": "Search",
     "search-mealie": "Search Mealie (press /)",
diff --git a/frontend/lib/api/admin/admin-households.ts b/frontend/lib/api/admin/admin-households.ts
new file mode 100644
index 00000000000..1e49723b760
--- /dev/null
+++ b/frontend/lib/api/admin/admin-households.ts
@@ -0,0 +1,13 @@
+import { BaseCRUDAPI } from "../base/base-clients";
+import { HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin } from "~/lib/api/types/household";
+const prefix = "/api";
+
+const routes = {
+  adminHouseholds: `${prefix}/admin/households`,
+  adminHouseholdsId: (id: string) => `${prefix}/admin/households/${id}`,
+};
+
+export class AdminHouseholdsApi extends BaseCRUDAPI<HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin> {
+  baseRoute: string = routes.adminHouseholds;
+  itemRoute = routes.adminHouseholdsId;
+}
diff --git a/frontend/lib/api/client-admin.ts b/frontend/lib/api/client-admin.ts
index a0bbca8f80f..bf151d390ed 100644
--- a/frontend/lib/api/client-admin.ts
+++ b/frontend/lib/api/client-admin.ts
@@ -1,5 +1,6 @@
 import { AdminAboutAPI } from "./admin/admin-about";
 import { AdminUsersApi } from "./admin/admin-users";
+import { AdminHouseholdsApi } from "./admin/admin-households";
 import { AdminGroupsApi } from "./admin/admin-groups";
 import { AdminBackupsApi } from "./admin/admin-backups";
 import { AdminMaintenanceApi } from "./admin/admin-maintenance";
@@ -9,6 +10,7 @@ import { ApiRequestInstance } from "~/lib/api/types/non-generated";
 export class AdminAPI {
   public about: AdminAboutAPI;
   public users: AdminUsersApi;
+  public households: AdminHouseholdsApi;
   public groups: AdminGroupsApi;
   public backups: AdminBackupsApi;
   public maintenance: AdminMaintenanceApi;
@@ -17,6 +19,7 @@ export class AdminAPI {
   constructor(requests: ApiRequestInstance) {
     this.about = new AdminAboutAPI(requests);
     this.users = new AdminUsersApi(requests);
+    this.households = new AdminHouseholdsApi(requests);
     this.groups = new AdminGroupsApi(requests);
     this.backups = new AdminBackupsApi(requests);
     this.maintenance = new AdminMaintenanceApi(requests);
diff --git a/frontend/lib/api/public/explore.ts b/frontend/lib/api/public/explore.ts
index 51f7d71357e..7a26a6380b4 100644
--- a/frontend/lib/api/public/explore.ts
+++ b/frontend/lib/api/public/explore.ts
@@ -4,6 +4,7 @@ import { PublicRecipeApi } from "./explore/recipes";
 import { PublicFoodsApi } from "./explore/foods";
 import { PublicCategoriesApi, PublicTagsApi, PublicToolsApi } from "./explore/organizers";
 import { PublicCookbooksApi } from "./explore/cookbooks";
+import { PublicHouseholdApi } from "./explore/households";
 
 export class ExploreApi extends BaseAPI {
   public recipes: PublicRecipeApi;
@@ -12,6 +13,7 @@ export class ExploreApi extends BaseAPI {
   public categories: PublicCategoriesApi;
   public tags: PublicTagsApi;
   public tools: PublicToolsApi;
+  public households: PublicHouseholdApi
 
   constructor(requests: ApiRequestInstance, groupSlug: string) {
     super(requests);
@@ -21,5 +23,6 @@ export class ExploreApi extends BaseAPI {
     this.categories = new PublicCategoriesApi(requests, groupSlug);
     this.tags = new PublicTagsApi(requests, groupSlug);
     this.tools = new PublicToolsApi(requests, groupSlug);
+    this.households = new PublicHouseholdApi(requests, groupSlug);
   }
 }
diff --git a/frontend/lib/api/public/explore/households.ts b/frontend/lib/api/public/explore/households.ts
new file mode 100644
index 00000000000..188d75053c4
--- /dev/null
+++ b/frontend/lib/api/public/explore/households.ts
@@ -0,0 +1,20 @@
+import { BaseCRUDAPIReadOnly } from "~/lib/api/base/base-clients";
+import { HouseholdSummary } from "~/lib/api/types/household";
+import { ApiRequestInstance, PaginationData } from "~/lib/api/types/non-generated";
+
+const prefix = "/api";
+const exploreGroupSlug = (groupSlug: string | number) => `${prefix}/explore/groups/${groupSlug}`
+
+const routes = {
+  householdsGroupSlug: (groupSlug: string | number) => `${exploreGroupSlug(groupSlug)}/households`,
+  householdsGroupSlugHouseholdSlug: (groupSlug: string | number, householdSlug: string | number) => `${exploreGroupSlug(groupSlug)}/households/${householdSlug}`,
+};
+
+export class PublicHouseholdApi extends BaseCRUDAPIReadOnly<HouseholdSummary> {
+  baseRoute = routes.householdsGroupSlug(this.groupSlug);
+  itemRoute = (itemId: string | number) => routes.householdsGroupSlugHouseholdSlug(this.groupSlug, itemId);
+
+  constructor(requests: ApiRequestInstance, private readonly groupSlug: string) {
+    super(requests);
+  }
+}
diff --git a/frontend/lib/api/user/groups.ts b/frontend/lib/api/user/groups.ts
index 5dc85110abb..fcd0a70d093 100644
--- a/frontend/lib/api/user/groups.ts
+++ b/frontend/lib/api/user/groups.ts
@@ -1,6 +1,5 @@
 import { BaseCRUDAPI } from "../base/base-clients";
 import { GroupBase, GroupInDB, GroupSummary, UserSummary } from "~/lib/api/types/user";
-import { HouseholdSummary } from "~/lib/api/types/household";
 import {
   GroupAdminUpdate,
   GroupStorage,
@@ -15,8 +14,6 @@ const routes = {
   groupsSelf: `${prefix}/groups/self`,
   preferences: `${prefix}/groups/preferences`,
   storage: `${prefix}/groups/storage`,
-  households: `${prefix}/groups/households`,
-  householdsId: (id: string | number) => `${prefix}/groups/households/${id}`,
   membersHouseholdId: (householdId: string | number | null) => {
     return householdId ?
       `${prefix}/households/members?householdId=${householdId}` :
@@ -47,14 +44,6 @@ export class GroupAPI extends BaseCRUDAPI<GroupBase, GroupInDB, GroupAdminUpdate
     return await this.requests.get<UserSummary[]>(routes.membersHouseholdId(householdId));
   }
 
-  async fetchHouseholds() {
-    return await this.requests.get<HouseholdSummary[]>(routes.households);
-  }
-
-  async fetchHousehold(householdId: string | number) {
-    return await this.requests.get<HouseholdSummary>(routes.householdsId(householdId));
-  }
-
   async storage() {
     return await this.requests.get<GroupStorage>(routes.storage);
   }
diff --git a/frontend/lib/api/user/households.ts b/frontend/lib/api/user/households.ts
index b1909e78b08..22fcad7d07f 100644
--- a/frontend/lib/api/user/households.ts
+++ b/frontend/lib/api/user/households.ts
@@ -1,21 +1,20 @@
-import { BaseCRUDAPI } from "../base/base-clients";
+import { BaseCRUDAPIReadOnly } from "../base/base-clients";
 import { UserOut } from "~/lib/api/types/user";
 import {
-  HouseholdCreate,
   HouseholdInDB,
-  UpdateHouseholdAdmin,
   HouseholdStatistics,
   ReadHouseholdPreferences,
   SetPermissions,
   UpdateHouseholdPreferences,
   CreateInviteToken,
   ReadInviteToken,
+  HouseholdSummary,
 } from "~/lib/api/types/household";
 
 const prefix = "/api";
 
 const routes = {
-  households: `${prefix}/admin/households`,
+  households: `${prefix}/groups/households`,
   householdsSelf: `${prefix}/households/self`,
   members: `${prefix}/households/members`,
   permissions: `${prefix}/households/permissions`,
@@ -24,13 +23,13 @@ const routes = {
   statistics: `${prefix}/households/statistics`,
   invitation: `${prefix}/households/invitations`,
 
-  householdsId: (id: string | number) => `${prefix}/admin/households/${id}`,
+  householdsId: (id: string | number) => `${prefix}/groups/households/${id}`,
 };
 
-export class HouseholdAPI extends BaseCRUDAPI<HouseholdCreate, HouseholdInDB, UpdateHouseholdAdmin> {
+export class HouseholdAPI extends BaseCRUDAPIReadOnly<HouseholdSummary> {
   baseRoute = routes.households;
   itemRoute = routes.householdsId;
-  /** Returns the Group Data for the Current User
+  /** Returns the Household Data for the Current User
    */
   async getCurrentUserHousehold() {
     return await this.requests.get<HouseholdInDB>(routes.householdsSelf);
diff --git a/frontend/lib/api/user/recipes/recipe.ts b/frontend/lib/api/user/recipes/recipe.ts
index 8aaeb317295..8e3b673b6de 100644
--- a/frontend/lib/api/user/recipes/recipe.ts
+++ b/frontend/lib/api/user/recipes/recipe.ts
@@ -56,13 +56,14 @@ const routes = {
 };
 
 export type RecipeSearchQuery = {
-  search: string;
+  search?: string;
   orderDirection?: "asc" | "desc";
   groupId?: string;
 
   queryFilter?: string;
 
   cookbook?: string;
+  households?: string[];
 
   categories?: string[];
   requireAllCategories?: boolean;
diff --git a/frontend/pages/admin/manage/households/_id.vue b/frontend/pages/admin/manage/households/_id.vue
index 3e897cff2b1..f1fac388e4f 100644
--- a/frontend/pages/admin/manage/households/_id.vue
+++ b/frontend/pages/admin/manage/households/_id.vue
@@ -45,7 +45,7 @@
 import { defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
 import HouseholdPreferencesEditor from "~/components/Domain/Household/HouseholdPreferencesEditor.vue";
 import { useGroups } from "~/composables/use-groups";
-import { useUserApi } from "~/composables/api";
+import { useAdminApi } from "~/composables/api";
 import { alert } from "~/composables/use-toast";
 import { validators } from "~/composables/use-validators";
 import { HouseholdInDB } from "~/lib/api/types/household";
@@ -68,14 +68,14 @@ export default defineComponent({
 
     const refHouseholdEditForm = ref<VForm | null>(null);
 
-    const userApi = useUserApi();
+    const adminApi = useAdminApi();
 
     const household = ref<HouseholdInDB | null>(null);
 
     const userError = ref(false);
 
     onMounted(async () => {
-      const { data, error } = await userApi.households.getOne(householdId);
+      const { data, error } = await adminApi.households.getOne(householdId);
 
       if (error?.response?.status === 404) {
         alert.error(i18n.tc("user.user-not-found"));
@@ -92,7 +92,7 @@ export default defineComponent({
         return;
       }
 
-      const { response, data } = await userApi.households.updateOne(household.value.id, household.value);
+      const { response, data } = await adminApi.households.updateOne(household.value.id, household.value);
       if (response?.status === 200 && data) {
         household.value = data;
         alert.success(i18n.tc("settings.settings-updated"));
diff --git a/frontend/pages/admin/manage/households/index.vue b/frontend/pages/admin/manage/households/index.vue
index 02c36914c80..122ba20b192 100644
--- a/frontend/pages/admin/manage/households/index.vue
+++ b/frontend/pages/admin/manage/households/index.vue
@@ -88,7 +88,7 @@
 import { defineComponent, reactive, toRefs, useContext, useRouter } from "@nuxtjs/composition-api";
 import { fieldTypes } from "~/composables/forms";
 import { useGroups } from "~/composables/use-groups";
-import { useHouseholds } from "~/composables/use-households";
+import { useAdminHouseholds } from "~/composables/use-households";
 import { validators } from "~/composables/use-validators";
 import { HouseholdInDB } from "~/lib/api/types/household";
 
@@ -97,7 +97,7 @@ export default defineComponent({
   setup() {
     const { i18n } = useContext();
     const { groups } = useGroups();
-    const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useHouseholds();
+    const { households, refreshAllHouseholds, deleteHousehold, createHousehold } = useAdminHouseholds();
 
     const state = reactive({
       createDialog: false,
diff --git a/frontend/pages/admin/manage/users/_id.vue b/frontend/pages/admin/manage/users/_id.vue
index 6982658a668..eac8b5bf9a6 100644
--- a/frontend/pages/admin/manage/users/_id.vue
+++ b/frontend/pages/admin/manage/users/_id.vue
@@ -80,7 +80,7 @@
 import { computed, defineComponent, useRoute, onMounted, ref, useContext } from "@nuxtjs/composition-api";
 import { useAdminApi, useUserApi } from "~/composables/api";
 import { useGroups } from "~/composables/use-groups";
-import { useHouseholds } from "~/composables/use-households";
+import { useAdminHouseholds } from "~/composables/use-households";
 import { alert } from "~/composables/use-toast";
 import { useUserForm } from "~/composables/use-users";
 import { validators } from "~/composables/use-validators";
@@ -92,7 +92,7 @@ export default defineComponent({
   setup() {
     const { userForm } = useUserForm();
     const { groups } = useGroups();
-    const { useHouseholdsInGroup } = useHouseholds();
+    const { useHouseholdsInGroup } = useAdminHouseholds();
     const { i18n } = useContext();
     const route = useRoute();
 
diff --git a/frontend/pages/admin/manage/users/create.vue b/frontend/pages/admin/manage/users/create.vue
index 0d0e380e239..2dc0cc19af1 100644
--- a/frontend/pages/admin/manage/users/create.vue
+++ b/frontend/pages/admin/manage/users/create.vue
@@ -50,7 +50,7 @@
 import { computed, defineComponent, useRouter, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
 import { useAdminApi } from "~/composables/api";
 import { useGroups } from "~/composables/use-groups";
-import { useHouseholds } from "~/composables/use-households";
+import { useAdminHouseholds } from "~/composables/use-households";
 import { useUserForm } from "~/composables/use-users";
 import { validators } from "~/composables/use-validators";
 import { VForm } from "~/types/vuetify";
@@ -60,7 +60,7 @@ export default defineComponent({
   setup() {
     const { userForm } = useUserForm();
     const { groups } = useGroups();
-    const { useHouseholdsInGroup } = useHouseholds();
+    const { useHouseholdsInGroup } = useAdminHouseholds();
     const router = useRouter();
 
     // ==============================================
diff --git a/frontend/pages/admin/setup.vue b/frontend/pages/admin/setup.vue
index b244dcdf13e..ef8e25c2a10 100644
--- a/frontend/pages/admin/setup.vue
+++ b/frontend/pages/admin/setup.vue
@@ -94,7 +94,7 @@
 
 <script lang="ts">
 import { computed, defineComponent, ref, useContext, useRouter } from "@nuxtjs/composition-api";
-import { useUserApi } from "~/composables/api";
+import { useAdminApi, useUserApi } from "~/composables/api";
 import { useLocales } from "~/composables/use-locales";
 import { alert } from "~/composables/use-toast";
 import { useUserRegistrationForm } from "~/composables/use-users/user-registration-form";
@@ -108,7 +108,8 @@ export default defineComponent({
     // ================================================================
     // Setup
     const { $auth, $globals, i18n } = useContext();
-    const api = useUserApi();
+    const userApi = useUserApi();
+    const adminApi = useAdminApi();
 
     const groupSlug = computed(() => $auth.user?.groupSlug);
     const { locale } = useLocales();
@@ -264,7 +265,7 @@ export default defineComponent({
 
     async function updateUser() {
       // @ts-ignore-next-line user will never be null here
-      const { response } = await api.users.updateOne($auth.user?.id, {
+      const { response } = await userApi.users.updateOne($auth.user?.id, {
         ...$auth.user,
         email: accountDetails.email.value,
         username: accountDetails.username.value,
@@ -285,7 +286,7 @@ export default defineComponent({
     }
 
     async function updatePassword() {
-      const { response } = await api.users.changePassword({
+      const { response } = await userApi.users.changePassword({
         currentPassword: "MyPassword",
         newPassword: credentials.password1.value,
       });
@@ -303,7 +304,7 @@ export default defineComponent({
 
     async function updateGroup() {
       // @ts-ignore-next-line user will never be null here
-      const { data } = await api.groups.getOne($auth.user?.groupId);
+      const { data } = await userApi.groups.getOne($auth.user?.groupId);
       if (!data || !data.preferences) {
         alert.error(i18n.tc("events.something-went-wrong"));
         return;
@@ -320,7 +321,7 @@ export default defineComponent({
       }
 
       // @ts-ignore-next-line user will never be null here
-      const { response } = await api.groups.updateOne($auth.user?.groupId, payload);
+      const { response } = await userApi.groups.updateOne($auth.user?.groupId, payload);
       if (!response || response.status !== 200) {
         alert.error(i18n.tc("events.something-went-wrong"));
       }
@@ -328,7 +329,7 @@ export default defineComponent({
 
     async function updateHousehold() {
       // @ts-ignore-next-line user will never be null here
-      const { data } = await api.households.getOne($auth.user?.householdId);
+      const { data } = await adminApi.households.getOne($auth.user?.householdId);
       if (!data || !data.preferences) {
         alert.error(i18n.tc("events.something-went-wrong"));
         return;
@@ -346,28 +347,28 @@ export default defineComponent({
       }
 
       // @ts-ignore-next-line user will never be null here
-      const { response } = await api.households.updateOne($auth.user?.householdId, payload);
+      const { response } = await adminApi.households.updateOne($auth.user?.householdId, payload);
       if (!response || response.status !== 200) {
         alert.error(i18n.tc("events.something-went-wrong"));
       }
     }
 
     async function seedFoods() {
-      const { response } = await api.seeders.foods({ locale: locale.value })
+      const { response } = await userApi.seeders.foods({ locale: locale.value })
       if (!response || response.status !== 200) {
         alert.error(i18n.tc("events.something-went-wrong"));
       }
     }
 
     async function seedUnits() {
-      const { response } = await api.seeders.units({ locale: locale.value })
+      const { response } = await userApi.seeders.units({ locale: locale.value })
       if (!response || response.status !== 200) {
         alert.error(i18n.tc("events.something-went-wrong"));
       }
     }
 
     async function seedLabels() {
-      const { response } = await api.seeders.labels({ locale: locale.value })
+      const { response } = await userApi.seeders.labels({ locale: locale.value })
       if (!response || response.status !== 200) {
         alert.error(i18n.tc("events.something-went-wrong"));
       }
diff --git a/frontend/pages/g/_groupSlug/r/_slug/ingredient-parser.vue b/frontend/pages/g/_groupSlug/r/_slug/ingredient-parser.vue
index 357be89c18a..9aa4518e821 100644
--- a/frontend/pages/g/_groupSlug/r/_slug/ingredient-parser.vue
+++ b/frontend/pages/g/_groupSlug/r/_slug/ingredient-parser.vue
@@ -272,12 +272,10 @@ export default defineComponent({
     const errors = ref<Error[]>([]);
 
     function checkForUnit(unit?: IngredientUnit | CreateIngredientUnit) {
-      // @ts-expect-error; we're just checking if there's an id on this unit and returning a boolean
       return !!unit?.id;
     }
 
     function checkForFood(food?: IngredientFood | CreateIngredientFood) {
-      // @ts-expect-error; we're just checking if there's an id on this food and returning a boolean
       return !!food?.id;
     }
 
diff --git a/frontend/pages/g/_groupSlug/recipes/categories/index.vue b/frontend/pages/g/_groupSlug/recipes/categories/index.vue
index 1cc94cf8681..452f672d8b7 100644
--- a/frontend/pages/g/_groupSlug/recipes/categories/index.vue
+++ b/frontend/pages/g/_groupSlug/recipes/categories/index.vue
@@ -1,8 +1,8 @@
 <template>
   <v-container>
     <RecipeOrganizerPage
-      v-if="items"
-      :items="items"
+      v-if="store"
+      :items="store"
       :icon="$globals.icons.categories"
       item-type="categories"
       @delete="actions.deleteOne"
@@ -24,10 +24,10 @@ export default defineComponent({
   },
   middleware: ["auth", "group-only"],
   setup() {
-    const { items, actions } = useCategoryStore();
+    const { store, actions } = useCategoryStore();
 
     return {
-      items,
+      store,
       actions,
     };
   },
diff --git a/frontend/pages/g/_groupSlug/recipes/tags/index.vue b/frontend/pages/g/_groupSlug/recipes/tags/index.vue
index 861fead9fac..ddd1ff02db7 100644
--- a/frontend/pages/g/_groupSlug/recipes/tags/index.vue
+++ b/frontend/pages/g/_groupSlug/recipes/tags/index.vue
@@ -1,8 +1,8 @@
 <template>
   <v-container>
     <RecipeOrganizerPage
-      v-if="items"
-      :items="items"
+      v-if="store"
+      :items="store"
       :icon="$globals.icons.tags"
       item-type="tags"
       @delete="actions.deleteOne"
@@ -24,10 +24,10 @@ export default defineComponent({
   },
   middleware: ["auth", "group-only"],
   setup() {
-    const { items, actions } = useTagStore();
+    const { store, actions } = useTagStore();
 
     return {
-      items,
+      store,
       actions,
     };
   },
diff --git a/frontend/pages/g/_groupSlug/recipes/tools/index.vue b/frontend/pages/g/_groupSlug/recipes/tools/index.vue
index ff2eaa8b205..5ec786051f5 100644
--- a/frontend/pages/g/_groupSlug/recipes/tools/index.vue
+++ b/frontend/pages/g/_groupSlug/recipes/tools/index.vue
@@ -29,7 +29,7 @@ export default defineComponent({
 
     return {
       dialog,
-      tools: toolStore.items,
+      tools: toolStore.store,
       actions: toolStore.actions,
     };
   },
diff --git a/frontend/pages/group/data/categories.vue b/frontend/pages/group/data/categories.vue
index 22ede431fae..271ec53bacb 100644
--- a/frontend/pages/group/data/categories.vue
+++ b/frontend/pages/group/data/categories.vue
@@ -81,6 +81,7 @@
       :headers.sync="tableHeaders"
       :data="categories || []"
       :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
+      initial-sort="name"
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @delete-selected="bulkDeleteEventHandler"
@@ -198,7 +199,7 @@ export default defineComponent({
       state,
       tableConfig,
       tableHeaders,
-      categories: categoryStore.items,
+      categories: categoryStore.store,
       validators,
 
       // create
diff --git a/frontend/pages/group/data/foods.vue b/frontend/pages/group/data/foods.vue
index 290b7dcef8e..cb41a09bd2d 100644
--- a/frontend/pages/group/data/foods.vue
+++ b/frontend/pages/group/data/foods.vue
@@ -241,6 +241,8 @@
         {icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'},
         {icon: $globals.icons.tags, text: $tc('data-pages.labels.assign-label'), event: 'assign-selected'}
       ]"
+      initial-sort="createdAt"
+      initial-sort-desc
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @create-one="createEventHandler"
@@ -264,6 +266,9 @@
           {{ item.onHand ? $globals.icons.check : $globals.icons.close }}
         </v-icon>
       </template>
+      <template #item.createdAt="{ item }">
+        {{ formatDate(item.createdAt) }}
+      </template>
       <template #button-bottom>
         <BaseButton @click="seedDialog = true">
           <template #icon> {{ $globals.icons.database }} </template>
@@ -326,8 +331,21 @@ export default defineComponent({
         value: "onHand",
         show: true,
       },
+      {
+        text: i18n.tc("general.date-added"),
+        value: "createdAt",
+        show: false,
+      }
     ];
 
+    function formatDate(date: string) {
+      try {
+        return i18n.d(Date.parse(date), "medium");
+      } catch {
+        return "";
+      }
+    }
+
     const foodStore = useFoodStore();
 
     // ===============================================================
@@ -453,7 +471,7 @@ export default defineComponent({
     // ============================================================
     // Labels
 
-    const { labels: allLabels } = useLabelStore();
+    const { store: allLabels } = useLabelStore();
 
     // ============================================================
     // Seed
@@ -501,16 +519,15 @@ export default defineComponent({
       bulkAssignTarget.value = [];
       bulkAssignLabelId.value = undefined;
       foodStore.actions.refresh();
-      // reload page, because foodStore.actions.refresh() does not update the table, reactivity for this seems to be broken (again)
-      document.location.reload();
     }
 
     return {
       tableConfig,
       tableHeaders,
-      foods: foodStore.foods,
+      foods: foodStore.store,
       allLabels,
       validators,
+      formatDate,
       // Create
       createDialog,
       domNewFoodForm,
diff --git a/frontend/pages/group/data/labels.vue b/frontend/pages/group/data/labels.vue
index ece501144fe..d30af8c1838 100644
--- a/frontend/pages/group/data/labels.vue
+++ b/frontend/pages/group/data/labels.vue
@@ -115,6 +115,7 @@
       :headers.sync="tableHeaders"
       :data="labels || []"
       :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
+      initial-sort="name"
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @delete-selected="bulkDeleteEventHandler"
@@ -271,7 +272,7 @@ export default defineComponent({
       state,
       tableConfig,
       tableHeaders,
-      labels: labelStore.labels,
+      labels: labelStore.store,
       validators,
 
       // create
diff --git a/frontend/pages/group/data/recipe-actions.vue b/frontend/pages/group/data/recipe-actions.vue
index 2b4402dc8a9..6a753c520fc 100644
--- a/frontend/pages/group/data/recipe-actions.vue
+++ b/frontend/pages/group/data/recipe-actions.vue
@@ -101,6 +101,7 @@
       :headers.sync="tableHeaders"
       :data="actions || []"
       :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
+      initial-sort="title"
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @delete-selected="bulkDeleteEventHandler"
diff --git a/frontend/pages/group/data/tags.vue b/frontend/pages/group/data/tags.vue
index 9e33049e801..73cc4ddbcd1 100644
--- a/frontend/pages/group/data/tags.vue
+++ b/frontend/pages/group/data/tags.vue
@@ -81,6 +81,7 @@
       :headers.sync="tableHeaders"
       :data="tags || []"
       :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
+      initial-sort="name"
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @delete-selected="bulkDeleteEventHandler"
@@ -199,7 +200,7 @@ export default defineComponent({
       state,
       tableConfig,
       tableHeaders,
-      tags: tagStore.items,
+      tags: tagStore.store,
       validators,
 
       // create
diff --git a/frontend/pages/group/data/tools.vue b/frontend/pages/group/data/tools.vue
index 358c3a52935..ac769c1a2db 100644
--- a/frontend/pages/group/data/tools.vue
+++ b/frontend/pages/group/data/tools.vue
@@ -83,6 +83,7 @@
       :headers.sync="tableHeaders"
       :data="tools || []"
       :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
+      initial-sort="name"
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @delete-selected="bulkDeleteEventHandler"
@@ -209,7 +210,7 @@ export default defineComponent({
       state,
       tableConfig,
       tableHeaders,
-      tools: toolStore.items,
+      tools: toolStore.store,
       validators,
 
       // create
diff --git a/frontend/pages/group/data/units.vue b/frontend/pages/group/data/units.vue
index 024a229fbdf..b5c6d5b3857 100644
--- a/frontend/pages/group/data/units.vue
+++ b/frontend/pages/group/data/units.vue
@@ -9,11 +9,11 @@
         </template>
       </i18n>
 
-        <v-autocomplete v-model="fromUnit" return-object :items="units" item-text="id" :label="$t('data-pages.units.source-unit')">
+        <v-autocomplete v-model="fromUnit" return-object :items="store" item-text="id" :label="$t('data-pages.units.source-unit')">
           <template #selection="{ item }"> {{ item.name }}</template>
           <template #item="{ item }"> {{ item.name }} </template>
         </v-autocomplete>
-        <v-autocomplete v-model="toUnit" return-object :items="units" item-text="id" :label="$t('data-pages.units.target-unit')">
+        <v-autocomplete v-model="toUnit" return-object :items="store" item-text="id" :label="$t('data-pages.units.target-unit')">
           <template #selection="{ item }"> {{ item.name }}</template>
           <template #item="{ item }"> {{ item.name }} </template>
         </v-autocomplete>
@@ -185,7 +185,7 @@
           </template>
         </v-autocomplete>
 
-        <v-alert v-if="units && units.length > 0" type="error" class="mb-0 text-body-2">
+        <v-alert v-if="store && store.length > 0" type="error" class="mb-0 text-body-2">
           {{ $t("data-pages.foods.seed-dialog-warning") }}
         </v-alert>
       </v-card-text>
@@ -196,8 +196,10 @@
     <CrudTable
       :table-config="tableConfig"
       :headers.sync="tableHeaders"
-      :data="units || []"
+      :data="store"
       :bulk-actions="[{icon: $globals.icons.delete, text: $tc('general.delete'), event: 'delete-selected'}]"
+      initial-sort="createdAt"
+      initial-sort-desc
       @delete-one="deleteEventHandler"
       @edit-one="editEventHandler"
       @create-one="createEventHandler"
@@ -221,6 +223,9 @@
           {{ item.fraction ? $globals.icons.check : $globals.icons.close }}
         </v-icon>
       </template>
+      <template #item.createdAt="{ item }">
+        {{ formatDate(item.createdAt) }}
+      </template>
       <template #button-bottom>
         <BaseButton @click="seedDialog = true">
           <template #icon> {{ $globals.icons.database }} </template>
@@ -292,9 +297,22 @@ export default defineComponent({
         value: "fraction",
         show: true,
       },
+      {
+        text: i18n.tc("general.date-added"),
+        value: "createdAt",
+        show: false,
+      },
     ];
 
-    const { units, actions: unitActions } = useUnitStore();
+    function formatDate(date: string) {
+      try {
+        return i18n.d(Date.parse(date), "medium");
+      } catch {
+        return "";
+      }
+    }
+
+    const { store, actions: unitActions } = useUnitStore();
 
     // ============================================================
     // Create Units
@@ -447,8 +465,9 @@ export default defineComponent({
     return {
       tableConfig,
       tableHeaders,
-      units,
+      store,
       validators,
+      formatDate,
       // Create
       createDialog,
       domNewUnitForm,
diff --git a/frontend/pages/shopping-lists/_id.vue b/frontend/pages/shopping-lists/_id.vue
index 09ba649fa3d..54c4a471b8f 100644
--- a/frontend/pages/shopping-lists/_id.vue
+++ b/frontend/pages/shopping-lists/_id.vue
@@ -602,9 +602,9 @@ export default defineComponent({
 
     const localLabels = ref<ShoppingListMultiPurposeLabelOut[]>()
 
-    const { labels: allLabels } = useLabelStore();
-    const { units: allUnits } = useUnitStore();
-    const { foods: allFoods } = useFoodStore();
+    const { store: allLabels } = useLabelStore();
+    const { store: allUnits } = useUnitStore();
+    const { store: allFoods } = useFoodStore();
 
     function getLabelColor(item: ShoppingListItemOut | null) {
       return item?.label?.color;
diff --git a/frontend/pages/user/_id/favorites.vue b/frontend/pages/user/_id/favorites.vue
index e7114a2b9c2..6f5e9dcc66c 100644
--- a/frontend/pages/user/_id/favorites.vue
+++ b/frontend/pages/user/_id/favorites.vue
@@ -5,34 +5,40 @@
       :icon="$globals.icons.heart"
       :title="$tc('user.user-favorites')"
       :recipes="recipes"
+      :query="query"
+      @sortRecipes="assignSorted"
+      @replaceRecipes="replaceRecipes"
+      @appendRecipes="appendRecipes"
+      @delete="removeRecipe"
     />
   </v-container>
 </template>
 
 <script lang="ts">
-import { defineComponent, useAsync, useRoute } from "@nuxtjs/composition-api";
+import { defineComponent, useRoute } from "@nuxtjs/composition-api";
 import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
+import { useLazyRecipes } from "~/composables/recipes";
 import { useLoggedInState } from "~/composables/use-logged-in-state";
-import { useUserApi } from "~/composables/api";
-import { useAsyncKey } from "~/composables/use-utils";
 
 export default defineComponent({
   components: { RecipeCardSection },
   middleware: "auth",
   setup() {
-    const api = useUserApi();
     const route = useRoute();
     const { isOwnGroup } = useLoggedInState();
 
     const userId = route.value.params.id;
-    const recipes = useAsync(async () => {
-      const { data } = await api.recipes.getAll(1, -1, { queryFilter: `favoritedBy.id = "${userId}"` });
-      return data?.items || null;
-    }, useAsyncKey());
+    const query = { queryFilter: `favoritedBy.id = "${userId}"` }
+    const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes();
 
     return {
+      query,
       recipes,
       isOwnGroup,
+      appendRecipes,
+      assignSorted,
+      removeRecipe,
+      replaceRecipes,
     };
   },
   head() {
diff --git a/mealie/repos/repository_recipes.py b/mealie/repos/repository_recipes.py
index e864c926e38..41c0cf10317 100644
--- a/mealie/repos/repository_recipes.py
+++ b/mealie/repos/repository_recipes.py
@@ -10,6 +10,7 @@
 from sqlalchemy.orm import InstrumentedAttribute
 from typing_extensions import Self
 
+from mealie.db.models.household.household import Household
 from mealie.db.models.recipe.category import Category
 from mealie.db.models.recipe.ingredient import RecipeIngredientModel
 from mealie.db.models.recipe.recipe import RecipeModel
@@ -155,6 +156,7 @@ def page_all(  # type: ignore
         tags: list[UUID4 | str] | None = None,
         tools: list[UUID4 | str] | None = None,
         foods: list[UUID4 | str] | None = None,
+        households: list[UUID4 | str] | None = None,
         require_all_categories=True,
         require_all_tags=True,
         require_all_tools=True,
@@ -170,6 +172,7 @@ def page_all(  # type: ignore
 
         if cookbook:
             cb_filters = self._build_recipe_filter(
+                households=[cookbook.household_id],
                 categories=extract_uuids(cookbook.categories),
                 tags=extract_uuids(cookbook.tags),
                 tools=extract_uuids(cookbook.tools),
@@ -183,11 +186,13 @@ def page_all(  # type: ignore
             category_ids = self._uuids_for_items(categories, Category)
             tag_ids = self._uuids_for_items(tags, Tag)
             tool_ids = self._uuids_for_items(tools, Tool)
+            household_ids = self._uuids_for_items(households, Household)
             filters = self._build_recipe_filter(
                 categories=category_ids,
                 tags=tag_ids,
                 tools=tool_ids,
                 foods=foods,
+                households=household_ids,
                 require_all_categories=require_all_categories,
                 require_all_tags=require_all_tags,
                 require_all_tools=require_all_tools,
@@ -245,6 +250,7 @@ def _build_recipe_filter(
         tags: list[UUID4] | None = None,
         tools: list[UUID4] | None = None,
         foods: list[UUID4] | None = None,
+        households: list[UUID4] | None = None,
         require_all_categories: bool = True,
         require_all_tags: bool = True,
         require_all_tools: bool = True,
@@ -278,6 +284,8 @@ def _build_recipe_filter(
                 fltr.extend(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id == food) for food in foods)
             else:
                 fltr.append(RecipeModel.recipe_ingredient.any(RecipeIngredientModel.food_id.in_(foods)))
+        if households:
+            fltr.append(RecipeModel.household_id.in_(households))
         return fltr
 
     def by_category_and_tags(
diff --git a/mealie/routes/_base/base_controllers.py b/mealie/routes/_base/base_controllers.py
index e5799b182e8..e7d4bd5e28b 100644
--- a/mealie/routes/_base/base_controllers.py
+++ b/mealie/routes/_base/base_controllers.py
@@ -1,7 +1,7 @@
 from abc import ABC
 from logging import Logger
 
-from fastapi import Depends
+from fastapi import Depends, HTTPException
 from pydantic import UUID4, ConfigDict
 from sqlalchemy.orm import Session
 
@@ -97,6 +97,12 @@ class BasePublicGroupExploreController(BasePublicController):
     def group_id(self) -> UUID4 | None | NotSet:
         return self.group.id
 
+    def get_public_household(self, household_slug_or_id: str | UUID4) -> HouseholdInDB:
+        household = self.repos.households.get_by_slug_or_id(household_slug_or_id)
+        if not household or household.preferences.private_household:
+            raise HTTPException(404, "household not found")
+        return household
+
     def get_explore_url_path(self, endpoint: str) -> str:
         if endpoint.startswith("/"):
             endpoint = endpoint[1:]
diff --git a/mealie/routes/explore/__init__.py b/mealie/routes/explore/__init__.py
index e329fc43f6a..15cc8112d63 100644
--- a/mealie/routes/explore/__init__.py
+++ b/mealie/routes/explore/__init__.py
@@ -3,6 +3,7 @@
 from . import (
     controller_public_cookbooks,
     controller_public_foods,
+    controller_public_households,
     controller_public_organizers,
     controller_public_recipes,
 )
@@ -11,6 +12,7 @@
 
 # group
 router.include_router(controller_public_foods.router, tags=["Explore: Foods"])
+router.include_router(controller_public_households.router, tags=["Explore: Households"])
 router.include_router(controller_public_organizers.categories_router, tags=["Explore: Categories"])
 router.include_router(controller_public_organizers.tags_router, tags=["Explore: Tags"])
 router.include_router(controller_public_organizers.tools_router, tags=["Explore: Tools"])
diff --git a/mealie/routes/explore/controller_public_households.py b/mealie/routes/explore/controller_public_households.py
new file mode 100644
index 00000000000..2edead21f8d
--- /dev/null
+++ b/mealie/routes/explore/controller_public_households.py
@@ -0,0 +1,35 @@
+from fastapi import APIRouter, Depends
+
+from mealie.routes._base import controller
+from mealie.routes._base.base_controllers import BasePublicGroupExploreController
+from mealie.schema.household.household import HouseholdSummary
+from mealie.schema.make_dependable import make_dependable
+from mealie.schema.response.pagination import PaginationBase, PaginationQuery
+
+router = APIRouter(prefix="/households")
+
+
+@controller(router)
+class PublicHouseholdsController(BasePublicGroupExploreController):
+    @property
+    def households(self):
+        return self.repos.households
+
+    @router.get("", response_model=PaginationBase[HouseholdSummary])
+    def get_all(
+        self, q: PaginationQuery = Depends(make_dependable(PaginationQuery))
+    ) -> PaginationBase[HouseholdSummary]:
+        public_filter = "(preferences.private_household = FALSE)"
+        if q.query_filter:
+            q.query_filter = f"({q.query_filter}) AND {public_filter}"
+        else:
+            q.query_filter = public_filter
+
+        response = self.households.page_all(pagination=q, override=HouseholdSummary)
+        response.set_pagination_guides(self.get_explore_url_path(router.url_path_for("get_all")), q.model_dump())
+        return response
+
+    @router.get("/{household_slug}", response_model=HouseholdSummary)
+    def get_household(self, household_slug: str) -> HouseholdSummary:
+        household = self.get_public_household(household_slug)
+        return household.cast(HouseholdSummary)
diff --git a/mealie/routes/explore/controller_public_recipes.py b/mealie/routes/explore/controller_public_recipes.py
index 9a443684058..11d1a356d5a 100644
--- a/mealie/routes/explore/controller_public_recipes.py
+++ b/mealie/routes/explore/controller_public_recipes.py
@@ -37,6 +37,7 @@ def get_all(
         tags: list[UUID4 | str] | None = Query(None),
         tools: list[UUID4 | str] | None = Query(None),
         foods: list[UUID4 | str] | None = Query(None),
+        households: list[UUID4 | str] | None = Query(None),
     ) -> PaginationBase[RecipeSummary]:
         cookbook_data: ReadCookBook | None = None
         recipes_repo = self.cross_household_recipes
@@ -76,6 +77,7 @@ def get_all(
             tags=tags,
             tools=tools,
             foods=foods,
+            households=households,
             require_all_categories=search_query.require_all_categories,
             require_all_tags=search_query.require_all_tags,
             require_all_tools=search_query.require_all_tools,
diff --git a/mealie/routes/groups/__init__.py b/mealie/routes/groups/__init__.py
index c42b7915b77..93514285030 100644
--- a/mealie/routes/groups/__init__.py
+++ b/mealie/routes/groups/__init__.py
@@ -1,6 +1,7 @@
 from fastapi import APIRouter
 
 from . import (
+    controller_group_households,
     controller_group_reports,
     controller_group_self_service,
     controller_labels,
@@ -10,6 +11,7 @@
 
 router = APIRouter()
 
+router.include_router(controller_group_households.router)
 router.include_router(controller_group_self_service.router)
 router.include_router(controller_migrations.router)
 router.include_router(controller_group_reports.router)
diff --git a/mealie/routes/groups/controller_group_households.py b/mealie/routes/groups/controller_group_households.py
new file mode 100644
index 00000000000..3fea64a69ff
--- /dev/null
+++ b/mealie/routes/groups/controller_group_households.py
@@ -0,0 +1,27 @@
+from fastapi import Depends, HTTPException
+
+from mealie.routes._base.base_controllers import BaseUserController
+from mealie.routes._base.controller import controller
+from mealie.routes._base.routers import UserAPIRouter
+from mealie.schema.household.household import HouseholdSummary
+from mealie.schema.response.pagination import PaginationBase, PaginationQuery
+
+router = UserAPIRouter(prefix="/groups/households", tags=["Groups: Households"])
+
+
+@controller(router)
+class GroupHouseholdsController(BaseUserController):
+    @router.get("", response_model=PaginationBase[HouseholdSummary])
+    def get_all_households(self, q: PaginationQuery = Depends(PaginationQuery)):
+        response = self.repos.households.page_all(pagination=q, override=HouseholdSummary)
+
+        response.set_pagination_guides(router.url_path_for("get_all_households"), q.model_dump())
+        return response
+
+    @router.get("/{household_slug}", response_model=HouseholdSummary)
+    def get_one_household(self, household_slug: str):
+        household = self.repos.households.get_by_slug_or_id(household_slug)
+
+        if not household:
+            raise HTTPException(status_code=404, detail="Household not found")
+        return household.cast(HouseholdSummary)
diff --git a/mealie/routes/groups/controller_group_self_service.py b/mealie/routes/groups/controller_group_self_service.py
index 2df5091bebc..2866329501c 100644
--- a/mealie/routes/groups/controller_group_self_service.py
+++ b/mealie/routes/groups/controller_group_self_service.py
@@ -1,6 +1,6 @@
 from functools import cached_property
 
-from fastapi import HTTPException, Query
+from fastapi import Query
 from pydantic import UUID4
 
 from mealie.routes._base.base_controllers import BaseUserController
@@ -8,9 +8,7 @@
 from mealie.routes._base.routers import UserAPIRouter
 from mealie.schema.group.group_preferences import ReadGroupPreferences, UpdateGroupPreferences
 from mealie.schema.group.group_statistics import GroupStorage
-from mealie.schema.household.household import HouseholdSummary
 from mealie.schema.response.pagination import PaginationQuery
-from mealie.schema.response.responses import ErrorResponse
 from mealie.schema.user.user import GroupSummary, UserSummary
 from mealie.services.group_services.group_service import GroupService
 
@@ -36,23 +34,6 @@ def get_group_members(self, household_id: UUID4 | None = Query(None, alias="hous
         private_users = self.repos.users.page_all(PaginationQuery(page=1, per_page=-1, query_filter=query_filter)).items
         return [user.cast(UserSummary) for user in private_users]
 
-    @router.get("/households", response_model=list[HouseholdSummary])
-    def get_group_households(self):
-        """Returns all households belonging to the current group"""
-
-        households = self.repos.households.page_all(PaginationQuery(page=1, per_page=-1)).items
-        return [household.cast(HouseholdSummary) for household in households]
-
-    @router.get("/households/{slug}", response_model=HouseholdSummary)
-    def get_group_household(self, slug: str):
-        """Returns a single household belonging to the current group"""
-
-        household = self.repos.households.get_by_slug_or_id(slug)
-        if not household:
-            raise HTTPException(status_code=404, detail=ErrorResponse.respond(message="No Entry Found"))
-
-        return household.cast(HouseholdSummary)
-
     @router.get("/preferences", response_model=ReadGroupPreferences)
     def get_group_preferences(self):
         return self.group.preferences
diff --git a/mealie/routes/recipe/recipe_crud_routes.py b/mealie/routes/recipe/recipe_crud_routes.py
index f0400c1c0c4..aa3fe8c8222 100644
--- a/mealie/routes/recipe/recipe_crud_routes.py
+++ b/mealie/routes/recipe/recipe_crud_routes.py
@@ -320,6 +320,7 @@ def get_all(
         tags: list[UUID4 | str] | None = Query(None),
         tools: list[UUID4 | str] | None = Query(None),
         foods: list[UUID4 | str] | None = Query(None),
+        households: list[UUID4 | str] | None = Query(None),
     ):
         cookbook_data: ReadCookBook | None = None
         if search_query.cookbook:
@@ -345,6 +346,7 @@ def get_all(
             tags=tags,
             tools=tools,
             foods=foods,
+            households=households,
             require_all_categories=search_query.require_all_categories,
             require_all_tags=search_query.require_all_tags,
             require_all_tools=search_query.require_all_tools,
diff --git a/tests/integration_tests/public_explorer_tests/test_public_households.py b/tests/integration_tests/public_explorer_tests/test_public_households.py
new file mode 100644
index 00000000000..23a319d1bd7
--- /dev/null
+++ b/tests/integration_tests/public_explorer_tests/test_public_households.py
@@ -0,0 +1,81 @@
+from uuid import UUID
+
+import pytest
+from fastapi.testclient import TestClient
+
+from mealie.schema.household.household import HouseholdCreate
+from mealie.schema.household.household_preferences import CreateHouseholdPreferences
+from mealie.services.household_services.household_service import HouseholdService
+from tests.utils import api_routes
+from tests.utils.factories import random_string
+from tests.utils.fixture_schemas import TestUser
+
+
+@pytest.mark.parametrize("is_private_group", [True, False])
+def test_get_all_households(api_client: TestClient, unique_user: TestUser, is_private_group: bool):
+    unique_user.repos.group_preferences.patch(UUID(unique_user.group_id), {"private_group": is_private_group})
+    households = [
+        HouseholdService.create_household(
+            unique_user.repos,
+            HouseholdCreate(name=random_string()),
+            CreateHouseholdPreferences(private_household=False),
+        )
+        for _ in range(5)
+    ]
+
+    response = api_client.get(api_routes.explore_groups_group_slug_households(unique_user.group_id))
+    if is_private_group:
+        assert response.status_code == 404
+    else:
+        assert response.status_code == 200
+        response_ids = [item["id"] for item in response.json()["items"]]
+        for household in households:
+            assert str(household.id) in response_ids
+
+
+@pytest.mark.parametrize("is_private_group", [True, False])
+def test_get_all_households_public_only(api_client: TestClient, unique_user: TestUser, is_private_group: bool):
+    unique_user.repos.group_preferences.patch(UUID(unique_user.group_id), {"private_group": is_private_group})
+    public_household = HouseholdService.create_household(
+        unique_user.repos,
+        HouseholdCreate(name=random_string()),
+        CreateHouseholdPreferences(private_household=False),
+    )
+    private_household = HouseholdService.create_household(
+        unique_user.repos,
+        HouseholdCreate(name=random_string()),
+        CreateHouseholdPreferences(private_household=True),
+    )
+
+    response = api_client.get(api_routes.explore_groups_group_slug_households(unique_user.group_id))
+    if is_private_group:
+        assert response.status_code == 404
+    else:
+        assert response.status_code == 200
+        response_ids = [item["id"] for item in response.json()["items"]]
+        assert str(public_household.id) in response_ids
+        assert str(private_household.id) not in response_ids
+
+
+@pytest.mark.parametrize("is_private_group", [True, False])
+@pytest.mark.parametrize("is_private_household", [True, False])
+def test_get_household(
+    api_client: TestClient, unique_user: TestUser, is_private_group: bool, is_private_household: bool
+):
+    unique_user.repos.group_preferences.patch(UUID(unique_user.group_id), {"private_group": is_private_group})
+    household = household = HouseholdService.create_household(
+        unique_user.repos,
+        HouseholdCreate(name=random_string()),
+        CreateHouseholdPreferences(private_household=is_private_household),
+    )
+
+    response = api_client.get(
+        api_routes.explore_groups_group_slug_households_household_slug(unique_user.group_id, household.slug),
+        headers=unique_user.token,
+    )
+
+    if is_private_group or is_private_household:
+        assert response.status_code == 404
+    else:
+        assert response.status_code == 200
+        assert response.json()["id"] == str(household.id)
diff --git a/tests/integration_tests/user_group_tests/test_group_self_service.py b/tests/integration_tests/user_group_tests/test_group_self_service.py
index 59bfacf33d9..0821030a06b 100644
--- a/tests/integration_tests/user_group_tests/test_group_self_service.py
+++ b/tests/integration_tests/user_group_tests/test_group_self_service.py
@@ -1,3 +1,5 @@
+import random
+
 from fastapi.testclient import TestClient
 
 from mealie.repos.repository_factory import AllRepositories
@@ -33,13 +35,10 @@ def test_get_group_members_filtered(api_client: TestClient, unique_user: TestUse
     assert str(h2_user.user_id) in all_ids
 
 
-def test_get_households(unfiltered_database: AllRepositories, api_client: TestClient, unique_user: TestUser):
-    households = [
-        unfiltered_database.households.create({"name": random_string(), "group_id": unique_user.group_id})
-        for _ in range(5)
-    ]
+def test_get_households(api_client: TestClient, unique_user: TestUser):
+    households = [unique_user.repos.households.create({"name": random_string()}) for _ in range(5)]
     response = api_client.get(api_routes.groups_households, headers=unique_user.token)
-    response_ids = [item["id"] for item in response.json()]
+    response_ids = [item["id"] for item in response.json()["items"]]
     for household in households:
         assert str(household.id) in response_ids
 
@@ -58,23 +57,22 @@ def test_get_households_filtered(unfiltered_database: AllRepositories, api_clien
     ]
 
     response = api_client.get(api_routes.groups_households, headers=unique_user.token)
-    response_ids = [item["id"] for item in response.json()]
+    response_ids = [item["id"] for item in response.json()["items"]]
     for household in group_1_households:
         assert str(household.id) in response_ids
     for household in group_2_households:
         assert str(household.id) not in response_ids
 
 
-def test_get_household(unfiltered_database: AllRepositories, api_client: TestClient, unique_user: TestUser):
-    group_1_id = unique_user.group_id
-    group_2_id = str(unfiltered_database.groups.create({"name": random_string()}).id)
-
-    group_1_household = unfiltered_database.households.create({"name": random_string(), "group_id": group_1_id})
-    group_2_household = unfiltered_database.households.create({"name": random_string(), "group_id": group_2_id})
+def test_get_one_household(api_client: TestClient, unique_user: TestUser):
+    households = [unique_user.repos.households.create({"name": random_string()}) for _ in range(5)]
+    household = random.choice(households)
 
-    response = api_client.get(api_routes.groups_households_slug(group_1_household.slug), headers=unique_user.token)
+    response = api_client.get(api_routes.groups_households_household_slug(household.slug), headers=unique_user.token)
     assert response.status_code == 200
-    assert response.json()["id"] == str(group_1_household.id)
+    assert response.json()["id"] == str(household.id)
+
 
-    response = api_client.get(api_routes.groups_households_slug(group_2_household.slug), headers=unique_user.token)
+def test_get_one_household_not_found(api_client: TestClient, unique_user: TestUser):
+    response = api_client.get(api_routes.groups_households_household_slug(random_string()), headers=unique_user.token)
     assert response.status_code == 404
diff --git a/tests/integration_tests/user_household_tests/test_group_cookbooks.py b/tests/integration_tests/user_household_tests/test_group_cookbooks.py
index b06decca0ae..11c2b64b1e2 100644
--- a/tests/integration_tests/user_household_tests/test_group_cookbooks.py
+++ b/tests/integration_tests/user_household_tests/test_group_cookbooks.py
@@ -1,4 +1,5 @@
 import random
+from collections.abc import Generator
 from dataclasses import dataclass
 from uuid import UUID
 
@@ -35,19 +36,20 @@ class TestCookbook:
 
 
 @pytest.fixture(scope="function")
-def cookbooks(unique_user: TestUser) -> list[TestCookbook]:
+def cookbooks(unique_user: TestUser) -> Generator[list[TestCookbook]]:
     database = unique_user.repos
 
     data: list[ReadCookBook] = []
     yield_data: list[TestCookbook] = []
     for _ in range(3):
         cb = database.cookbooks.create(SaveCookBook(**get_page_data(unique_user.group_id, unique_user.household_id)))
+        assert cb.slug
         data.append(cb)
         yield_data.append(TestCookbook(id=cb.id, slug=cb.slug, name=cb.name, data=cb.model_dump()))
 
     yield yield_data
 
-    for cb in yield_data:
+    for cb in data:
         try:
             database.cookbooks.delete(cb.id)
         except Exception:
diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py b/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py
index a04bbb4a321..4528bb21fe7 100644
--- a/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py
+++ b/tests/integration_tests/user_recipe_tests/test_recipe_cross_household.py
@@ -3,6 +3,9 @@
 import pytest
 from fastapi.testclient import TestClient
 
+from mealie.schema.cookbook.cookbook import SaveCookBook
+from mealie.schema.recipe.recipe import Recipe
+from mealie.schema.recipe.recipe_category import TagSave
 from tests.utils import api_routes
 from tests.utils.factories import random_string
 from tests.utils.fixture_schemas import TestUser
@@ -65,6 +68,38 @@ def test_get_all_recipes_includes_all_households(
     assert str(h2_recipe_id) in response_ids
 
 
+@pytest.mark.parametrize("is_private_household", [True, False])
+def test_get_all_recipes_with_household_filter(
+    api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
+):
+    household = unique_user.repos.households.get_one(h2_user.household_id)
+    assert household and household.preferences
+    household.preferences.private_household = is_private_household
+    unique_user.repos.household_preferences.update(household.id, household.preferences)
+
+    response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
+    assert response.status_code == 201
+    recipe = unique_user.repos.recipes.get_one(response.json())
+    assert recipe and recipe.id
+    recipe_id = recipe.id
+
+    response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token)
+    assert response.status_code == 201
+    h2_recipe = h2_user.repos.recipes.get_one(response.json())
+    assert h2_recipe and h2_recipe.id
+    h2_recipe_id = h2_recipe.id
+
+    response = api_client.get(
+        api_routes.recipes,
+        params={"households": [h2_recipe.household_id], "page": 1, "perPage": -1},
+        headers=unique_user.token,
+    )
+    assert response.status_code == 200
+    response_ids = {recipe["id"] for recipe in response.json()["items"]}
+    assert str(recipe_id) not in response_ids
+    assert str(h2_recipe_id) in response_ids
+
+
 @pytest.mark.parametrize("is_private_household", [True, False])
 def test_get_one_recipe_from_another_household(
     api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
@@ -220,3 +255,49 @@ def test_user_can_update_last_made_on_other_household(
     assert recipe["id"] == str(h2_recipe_id)
     new_last_made = recipe["lastMade"]
     assert new_last_made == now != old_last_made
+
+
+def test_cookbook_recipes_only_includes_current_households(
+    api_client: TestClient, unique_user: TestUser, h2_user: TestUser
+):
+    tag = unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
+    recipes = unique_user.repos.recipes.create_many(
+        [
+            Recipe(
+                user_id=unique_user.user_id,
+                group_id=unique_user.group_id,
+                name=random_string(),
+                tags=[tag],
+            )
+            for _ in range(3)
+        ]
+    )
+    other_recipes = h2_user.repos.recipes.create_many(
+        [
+            Recipe(
+                user_id=h2_user.user_id,
+                group_id=h2_user.group_id,
+                name=random_string(),
+            )
+            for _ in range(3)
+        ]
+    )
+
+    cookbook = unique_user.repos.cookbooks.create(
+        SaveCookBook(
+            name=random_string(),
+            group_id=unique_user.group_id,
+            household_id=unique_user.household_id,
+            tags=[tag],
+        )
+    )
+
+    response = api_client.get(api_routes.recipes, params={"cookbook": cookbook.slug}, headers=unique_user.token)
+    assert response.status_code == 200
+    recipes = [Recipe.model_validate(data) for data in response.json()["items"]]
+
+    fetched_recipe_ids = {recipe.id for recipe in recipes}
+    for recipe in recipes:
+        assert recipe.id in fetched_recipe_ids
+    for recipe in other_recipes:
+        assert recipe.id not in fetched_recipe_ids
diff --git a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py
index 779261ed2dc..bf4d343f45f 100644
--- a/tests/integration_tests/user_recipe_tests/test_recipe_crud.py
+++ b/tests/integration_tests/user_recipe_tests/test_recipe_crud.py
@@ -20,6 +20,7 @@
 from slugify import slugify
 
 from mealie.pkgs.safehttp.transport import AsyncSafeTransport
+from mealie.schema.cookbook.cookbook import SaveCookBook
 from mealie.schema.recipe.recipe import Recipe, RecipeCategory, RecipeSummary, RecipeTag
 from mealie.schema.recipe.recipe_category import CategorySave, TagSave
 from mealie.schema.recipe.recipe_notes import RecipeNote
@@ -791,3 +792,47 @@ def test_get_random_order(api_client: TestClient, unique_user: utils.TestUser):
     badparams: dict[str, int | str] = {"page": 1, "perPage": -1, "orderBy": "random"}
     response = api_client.get(api_routes.recipes, params=badparams, headers=unique_user.token)
     assert response.status_code == 422
+
+
+def test_get_cookbook_recipes(api_client: TestClient, unique_user: utils.TestUser):
+    tag = unique_user.repos.tags.create(TagSave(name=random_string(), group_id=unique_user.group_id))
+    cookbook_recipes = unique_user.repos.recipes.create_many(
+        [
+            Recipe(
+                user_id=unique_user.user_id,
+                group_id=unique_user.group_id,
+                name=random_string(),
+                tags=[tag],
+            )
+            for _ in range(3)
+        ]
+    )
+    other_recipes = unique_user.repos.recipes.create_many(
+        [
+            Recipe(
+                user_id=unique_user.user_id,
+                group_id=unique_user.group_id,
+                name=random_string(),
+            )
+            for _ in range(3)
+        ]
+    )
+
+    cookbook = unique_user.repos.cookbooks.create(
+        SaveCookBook(
+            name=random_string(),
+            group_id=unique_user.group_id,
+            household_id=unique_user.household_id,
+            tags=[tag],
+        )
+    )
+
+    response = api_client.get(api_routes.recipes, params={"cookbook": cookbook.slug}, headers=unique_user.token)
+    assert response.status_code == 200
+    recipes = [Recipe.model_validate(data) for data in response.json()["items"]]
+
+    fetched_recipe_ids = {recipe.id for recipe in recipes}
+    for recipe in cookbook_recipes:
+        assert recipe.id in fetched_recipe_ids
+    for recipe in other_recipes:
+        assert recipe.id not in fetched_recipe_ids
diff --git a/tests/utils/api_routes/__init__.py b/tests/utils/api_routes/__init__.py
index b881c34055c..587ca4abef3 100644
--- a/tests/utils/api_routes/__init__.py
+++ b/tests/utils/api_routes/__init__.py
@@ -247,6 +247,16 @@ def explore_groups_group_slug_foods_item_id(group_slug, item_id):
     return f"{prefix}/explore/groups/{group_slug}/foods/{item_id}"
 
 
+def explore_groups_group_slug_households(group_slug):
+    """`/api/explore/groups/{group_slug}/households`"""
+    return f"{prefix}/explore/groups/{group_slug}/households"
+
+
+def explore_groups_group_slug_households_household_slug(group_slug, household_slug):
+    """`/api/explore/groups/{group_slug}/households/{household_slug}`"""
+    return f"{prefix}/explore/groups/{group_slug}/households/{household_slug}"
+
+
 def explore_groups_group_slug_organizers_categories(group_slug):
     """`/api/explore/groups/{group_slug}/organizers/categories`"""
     return f"{prefix}/explore/groups/{group_slug}/organizers/categories"
@@ -292,9 +302,9 @@ def foods_item_id(item_id):
     return f"{prefix}/foods/{item_id}"
 
 
-def groups_households_slug(slug):
-    """`/api/groups/households/{slug}`"""
-    return f"{prefix}/groups/households/{slug}"
+def groups_households_household_slug(household_slug):
+    """`/api/groups/households/{household_slug}`"""
+    return f"{prefix}/groups/households/{household_slug}"
 
 
 def groups_labels_item_id(item_id):