diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4dcee7b..6569c1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
All notable changes to Epicure are documented here. This file is mirrored in-app at `/changelog` (and in the admin dashboard) via `apps/web/lib/changelog.ts` — update both together.
+## 0.77.0 — 2026-07-24 13:00
+
+### Added
+- New post-signup onboarding wizard: a quick 3-step flow (welcome, dietary preferences & allergens, notification opt-in) shown once for new accounts, skippable at every step. Existing accounts are unaffected — only new signups see it.
+
+### Fixed
+- Allergen preferences (the `user_allergens` table) had a GDPR-export read path but no way to actually set them — the onboarding wizard's dietary/allergen step is the first UI that writes to it.
+
## 0.76.0 — 2026-07-24 12:15
### Added
diff --git a/FEATURE_AUDIT.md b/FEATURE_AUDIT.md
index bdf7b1e..aa979b4 100644
--- a/FEATURE_AUDIT.md
+++ b/FEATURE_AUDIT.md
@@ -71,6 +71,7 @@ Status legend: **Exists** (fully working) · **Partial** (works but with a real
| Auto-deduct pantry on cook | Exists (closed 2026-07-24) | Both UI callers that previously hardcoded `deductFromPantry: false` (`batch-cook-dishes.tsx`, `meal-planner.tsx`) now pass `true`. The new general "Mark cooked" feature (see below) additionally exposes it as a per-cook checkbox, default checked, rather than a silent always-on. | `apps/web/app/api/v1/recipes/[id]/cooked/route.ts`, `apps/web/components/recipe/batch-cook-dishes.tsx`, `apps/web/components/meal-plan/meal-planner.tsx` |
| Mark recipe as cooked (any recipe, not just batch-cook) | Exists (new, 2026-07-24) | A recipe can be logged as cooked any number of times — each log is a new `cookingHistory` row, no unique constraint, this was already true at the schema level, just not exposed for plain (non-batch) recipes before. New dialog on the recipe page: date (defaults today, can backdate), servings, and a deduct-from-pantry toggle (default on). Shows a "cooked N times · last DATE" indicator once logged. | `apps/web/components/recipe/{mark-cooked-dialog,mark-cooked-section}.tsx`, `apps/web/app/api/v1/recipes/[id]/cooked/route.ts` (`cookedAt` field, new) |
| Billing/invoice details (full name, address, phone) | Exists (new, 2026-07-24) | New `userBillingDetails` table (1:1 with `users`), separate from the display `name` field. `fullName` is required by the form/API but nullable at the DB level (no backfill for existing users); address lines/city/postal code/country/phone are all optional. Lives under `Settings → Billing`, feeds future invoice generation — not wired into Stripe yet. | `packages/db/src/schema/billing.ts` (`userBillingDetails`), `apps/web/app/api/v1/users/me/billing-details/route.ts`, `apps/web/components/settings/billing-details-form.tsx` |
+| Post-signup onboarding wizard | Exists (new, 2026-07-24) | 3-step wizard (welcome → dietary/allergen prefs → notification opt-in) shown once per account, right after the app shell layout loads for a user with no `onboardingCompletedAt`. Skippable at every step; existing accounts were backfilled as already-onboarded so the wizard only appears for new signups. Also closes a real pre-existing gap: `userAllergens` had a table and a GDPR-export read, but **no write path at all** — this ships the first one. Dietary tags are a new `users.dietaryTags` jsonb column (mirrors `recipes.dietaryTags`'s 7-tag shape); neither is yet consumed by AI generation or recipe matching. | `apps/web/app/onboarding/page.tsx`, `apps/web/components/onboarding/onboarding-wizard.tsx`, `apps/web/app/api/v1/users/me/onboarding/route.ts`, `apps/web/app/(app)/layout.tsx` (redirect gate) |
| Expiring-soon tracking | Exists | With "use it up" recipe suggestions. The leftover-expiry push/email cron only covers batch-cook dishes — plain pantry items only get an in-app badge, no reminder. | `apps/web/components/pantry/expiring-soon-suggestions.tsx`, `apps/web/app/api/internal/cron/leftover-reminders/route.ts` |
| **Barcode scan (pantry)** | **Exists** | Real Open Food Facts API lookup by UPC/EAN | `apps/web/app/api/v1/pantry/scan/barcode` |
| Photo scan (pantry) | Exists | Vision-model based | `apps/web/app/api/v1/pantry/scan/photo` |
diff --git a/apps/web/app/(app)/layout.tsx b/apps/web/app/(app)/layout.tsx
index 4439a4f..d53b6c3 100644
--- a/apps/web/app/(app)/layout.tsx
+++ b/apps/web/app/(app)/layout.tsx
@@ -1,6 +1,19 @@
+import { headers } from "next/headers";
+import { redirect } from "next/navigation";
import { Nav } from "@/components/layout/nav";
+import { auth } from "@/lib/auth/server";
+import { db, users, eq } from "@epicure/db";
+
+export default async function AppLayout({ children }: { children: React.ReactNode }) {
+ const session = await auth.api.getSession({ headers: await headers() });
+ if (session) {
+ const dbUser = await db.query.users.findFirst({
+ where: eq(users.id, session.user.id),
+ columns: { onboardingCompletedAt: true },
+ });
+ if (!dbUser?.onboardingCompletedAt) redirect("/onboarding");
+ }
-export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<>
diff --git a/apps/web/app/api/v1/users/me/onboarding/route.ts b/apps/web/app/api/v1/users/me/onboarding/route.ts
new file mode 100644
index 0000000..00fc9f7
--- /dev/null
+++ b/apps/web/app/api/v1/users/me/onboarding/route.ts
@@ -0,0 +1,75 @@
+import { NextRequest, NextResponse } from "next/server";
+import { db, users, userAllergens, eq } from "@epicure/db";
+import { requireSession } from "@/lib/api-auth";
+import { z } from "zod";
+
+const ALLERGEN_TAGS = ["peanuts", "treeNuts", "dairy", "eggs", "shellfish", "fish", "soy", "gluten"] as const;
+
+const CompleteSchema = z.object({
+ dietaryTags: z
+ .object({
+ vegan: z.boolean().optional(),
+ vegetarian: z.boolean().optional(),
+ glutenFree: z.boolean().optional(),
+ dairyFree: z.boolean().optional(),
+ nutFree: z.boolean().optional(),
+ halal: z.boolean().optional(),
+ kosher: z.boolean().optional(),
+ })
+ .optional(),
+ allergens: z.array(z.enum(ALLERGEN_TAGS)).optional(),
+});
+
+export async function GET() {
+ const { session, response } = await requireSession();
+ if (response) return response;
+
+ const [dbUser, allergenRows] = await Promise.all([
+ db.query.users.findFirst({
+ where: eq(users.id, session!.user.id),
+ columns: { onboardingCompletedAt: true, dietaryTags: true },
+ }),
+ db.select().from(userAllergens).where(eq(userAllergens.userId, session!.user.id)),
+ ]);
+
+ return NextResponse.json({
+ data: {
+ completed: !!dbUser?.onboardingCompletedAt,
+ dietaryTags: dbUser?.dietaryTags ?? {},
+ allergens: allergenRows.map((r) => r.allergenTag),
+ },
+ });
+}
+
+// Marks onboarding done regardless of what (if anything) was filled in — a
+// user can skip every step and this still fires so the wizard never shows
+// again. dietaryTags/allergens are optional and only written if provided.
+export async function POST(req: NextRequest) {
+ const { session, response } = await requireSession();
+ if (response) return response;
+
+ const parsed = CompleteSchema.safeParse(await req.json().catch(() => ({})));
+ if (!parsed.success) {
+ return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
+ }
+
+ const { dietaryTags, allergens } = parsed.data;
+ const userId = session!.user.id;
+
+ await db
+ .update(users)
+ .set({
+ onboardingCompletedAt: new Date(),
+ ...(dietaryTags ? { dietaryTags } : {}),
+ })
+ .where(eq(users.id, userId));
+
+ if (allergens) {
+ await db.delete(userAllergens).where(eq(userAllergens.userId, userId));
+ if (allergens.length > 0) {
+ await db.insert(userAllergens).values(allergens.map((allergenTag) => ({ userId, allergenTag })));
+ }
+ }
+
+ return NextResponse.json({ ok: true });
+}
diff --git a/apps/web/app/onboarding/page.tsx b/apps/web/app/onboarding/page.tsx
new file mode 100644
index 0000000..9238fb3
--- /dev/null
+++ b/apps/web/app/onboarding/page.tsx
@@ -0,0 +1,22 @@
+import { redirect } from "next/navigation";
+import { headers } from "next/headers";
+import { auth } from "@/lib/auth/server";
+import { db, users, eq } from "@epicure/db";
+import { OnboardingWizard } from "@/components/onboarding/onboarding-wizard";
+
+export default async function OnboardingPage() {
+ const session = await auth.api.getSession({ headers: await headers() });
+ if (!session) redirect("/login");
+
+ const dbUser = await db.query.users.findFirst({
+ where: eq(users.id, session.user.id),
+ columns: { onboardingCompletedAt: true },
+ });
+ if (dbUser?.onboardingCompletedAt) redirect("/recipes");
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/components/onboarding/onboarding-wizard.tsx b/apps/web/components/onboarding/onboarding-wizard.tsx
new file mode 100644
index 0000000..5e5b36f
--- /dev/null
+++ b/apps/web/components/onboarding/onboarding-wizard.tsx
@@ -0,0 +1,161 @@
+"use client";
+
+import { useState } from "react";
+import { useRouter } from "next/navigation";
+import { useTranslations } from "next-intl";
+import { Sparkles } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Label } from "@/components/ui/label";
+import { PushSubscribeButton } from "@/components/pwa/push-subscribe-button";
+
+const DIETARY_TAGS = ["vegan", "vegetarian", "glutenFree", "dairyFree", "nutFree", "halal", "kosher"] as const;
+const ALLERGEN_TAGS = ["peanuts", "treeNuts", "dairy", "eggs", "shellfish", "fish", "soy", "gluten"] as const;
+
+type DietaryTag = (typeof DIETARY_TAGS)[number];
+type AllergenTag = (typeof ALLERGEN_TAGS)[number];
+
+const DIET_LABEL_KEYS: Record = {
+ vegan: "dietVegan", vegetarian: "dietVegetarian", glutenFree: "dietGlutenFree", dairyFree: "dietDairyFree",
+ nutFree: "dietNutFree", halal: "dietHalal", kosher: "dietKosher",
+};
+const ALLERGEN_LABEL_KEYS: Record = {
+ peanuts: "allergenPeanuts", treeNuts: "allergenTreeNuts", dairy: "allergenDairy", eggs: "allergenEggs",
+ shellfish: "allergenShellfish", fish: "allergenFish", soy: "allergenSoy", gluten: "allergenGluten",
+};
+
+const TOTAL_STEPS = 3;
+
+export function OnboardingWizard() {
+ const t = useTranslations("onboarding");
+ const tCommon = useTranslations("common");
+ const router = useRouter();
+ const [step, setStep] = useState(1);
+ const [dietaryTags, setDietaryTags] = useState>>({});
+ const [allergens, setAllergens] = useState>(new Set());
+ const [finishing, setFinishing] = useState(false);
+
+ function toggleDietaryTag(tag: DietaryTag, checked: boolean) {
+ setDietaryTags((prev) => ({ ...prev, [tag]: checked }));
+ }
+
+ function toggleAllergen(tag: AllergenTag, checked: boolean) {
+ setAllergens((prev) => {
+ const next = new Set(prev);
+ if (checked) next.add(tag);
+ else next.delete(tag);
+ return next;
+ });
+ }
+
+ async function finish(withData: boolean) {
+ setFinishing(true);
+ try {
+ await fetch("/api/v1/users/me/onboarding", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(withData ? { dietaryTags, allergens: Array.from(allergens) } : {}),
+ });
+ } catch {
+ // Onboarding completion isn't critical-path — if it fails, the user
+ // just sees the wizard once more next time. Not worth blocking or
+ // erroring loudly for a best-effort save.
+ } finally {
+ router.push("/recipes");
+ }
+ }
+
+ return (
+
+
+
+ {t("stepOf", { step, total: TOTAL_STEPS })}
+
+ {step === 1 && <> {t("welcomeTitle")}>}
+ {step === 2 && t("dietaryTitle")}
+ {step === 3 && t("notificationsTitle")}
+
+
+ {step === 1 && t("welcomeBody")}
+ {step === 2 && t("dietaryBody")}
+ {step === 3 && t("notificationsBody")}
+
+
+
+ {step === 2 && (
+
+
+
{t("dietaryTagsLabel")}
+
+ {DIETARY_TAGS.map((tag) => (
+
+ toggleDietaryTag(tag, checked === true)}
+ />
+ {t(DIET_LABEL_KEYS[tag])}
+
+ ))}
+
+
+
+
{t("allergensLabel")}
+
+ {ALLERGEN_TAGS.map((tag) => (
+
+ toggleAllergen(tag, checked === true)}
+ />
+ {t(ALLERGEN_LABEL_KEYS[tag])}
+
+ ))}
+
+
+
+ )}
+
+ {step === 3 && (
+
+
+
+ )}
+
+
+ {step === 1 && (
+ <>
+ { void finish(false); }}>
+ {tCommon("skip")}
+
+ setStep(2)}>{tCommon("continue")}
+ >
+ )}
+ {step === 2 && (
+ <>
+ {
+ setDietaryTags({});
+ setAllergens(new Set());
+ setStep(3);
+ }}
+ >
+ {tCommon("skip")}
+
+ setStep(3)}>{tCommon("continue")}
+ >
+ )}
+ {step === 3 && (
+ { void finish(true); }}>
+ {tCommon("finish")}
+
+ )}
+
+
+
+ );
+}
diff --git a/apps/web/components/ui/checkbox.tsx b/apps/web/components/ui/checkbox.tsx
new file mode 100644
index 0000000..4fcd847
--- /dev/null
+++ b/apps/web/components/ui/checkbox.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
+
+import { cn } from "@/lib/utils"
+import { CheckIcon } from "lucide-react"
+
+function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { Checkbox }
diff --git a/apps/web/lib/changelog.ts b/apps/web/lib/changelog.ts
index 8610171..79a6cea 100644
--- a/apps/web/lib/changelog.ts
+++ b/apps/web/lib/changelog.ts
@@ -1,5 +1,5 @@
// Mirrors CHANGELOG.md at the repo root — update both together.
-export const APP_VERSION = "0.76.0";
+export const APP_VERSION = "0.77.0";
export type ChangelogEntry = {
version: string;
@@ -11,6 +11,16 @@ export type ChangelogEntry = {
};
export const CHANGELOG: ChangelogEntry[] = [
+ {
+ version: "0.77.0",
+ date: "2026-07-24 13:00",
+ added: [
+ "New post-signup onboarding wizard: a quick 3-step flow (welcome, dietary preferences & allergens, notification opt-in) shown once for new accounts, skippable at every step. Existing accounts are unaffected — only new signups see it.",
+ ],
+ fixed: [
+ "Allergen preferences (the user_allergens table) had a GDPR-export read path but no way to actually set them — the onboarding wizard's dietary/allergen step is the first UI that writes to it.",
+ ],
+ },
{
version: "0.76.0",
date: "2026-07-24 12:15",
diff --git a/apps/web/lib/openapi.ts b/apps/web/lib/openapi.ts
index eb955de..af6d222 100644
--- a/apps/web/lib/openapi.ts
+++ b/apps/web/lib/openapi.ts
@@ -497,6 +497,18 @@ export function generateOpenApiSpec(): object {
caloriesKcal: z.number().int().min(0).optional(), proteinG: z.number().int().min(0).optional(),
carbsG: z.number().int().min(0).optional(), fatG: z.number().int().min(0).optional(),
}));
+ const OnboardingStatusRef = registry.register("OnboardingStatus", z.object({
+ completed: z.boolean(),
+ dietaryTags: z.record(z.string(), z.boolean()),
+ allergens: z.array(z.string()),
+ }));
+ const CompleteOnboardingRef = registry.register("CompleteOnboarding", z.object({
+ dietaryTags: z.object({
+ vegan: z.boolean().optional(), vegetarian: z.boolean().optional(), glutenFree: z.boolean().optional(),
+ dairyFree: z.boolean().optional(), nutFree: z.boolean().optional(), halal: z.boolean().optional(), kosher: z.boolean().optional(),
+ }).optional(),
+ allergens: z.array(z.enum(["peanuts", "treeNuts", "dairy", "eggs", "shellfish", "fish", "soy", "gluten"])).optional(),
+ }));
const BillingDetailsRef = registry.register("BillingDetailsMe", z.object({
id: z.string(), userId: z.string(),
fullName: z.string().nullable(), addressLine1: z.string().nullable(), addressLine2: z.string().nullable(),
@@ -558,6 +570,8 @@ export function generateOpenApiSpec(): object {
registry.registerPath({ method: "put", path: "/api/v1/users/me/nutrition-goals", summary: "Set your daily nutrition goals", security, request: { body: { content: { "application/json": { schema: UpdateNutritionGoalsRef } }, required: true } }, responses: { 200: { description: "Saved", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Invalid request", content: { "application/json": { schema: ApiErrorRef } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
registry.registerPath({ method: "get", path: "/api/v1/users/me/billing-details", summary: "Get your billing/invoice details", description: "Full name, address, and phone used on invoices — separate from your display name.", security, responses: { 200: { description: "Billing details or null", content: { "application/json": { schema: z.object({ data: BillingDetailsRef }) } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
registry.registerPath({ method: "put", path: "/api/v1/users/me/billing-details", summary: "Set your billing/invoice details", description: "fullName is required; addressLine1/2, city, postalCode, country, and phone are all optional.", security, request: { body: { content: { "application/json": { schema: UpdateBillingDetailsRef } }, required: true } }, responses: { 200: { description: "Saved", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Invalid request", content: { "application/json": { schema: ApiErrorRef } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
+ registry.registerPath({ method: "get", path: "/api/v1/users/me/onboarding", summary: "Get your post-signup onboarding status", security, responses: { 200: { description: "Status", content: { "application/json": { schema: z.object({ data: OnboardingStatusRef }) } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
+ registry.registerPath({ method: "post", path: "/api/v1/users/me/onboarding", summary: "Complete (or skip) the post-signup onboarding wizard", description: "Marks onboarding done regardless of what's in the body — call with an empty body to skip entirely. dietaryTags/allergens are only written when provided.", security, request: { body: { content: { "application/json": { schema: CompleteOnboardingRef } } } }, responses: { 200: { description: "Completed", content: { "application/json": { schema: z.object({ ok: z.boolean() }) } } }, 400: { description: "Invalid request", content: { "application/json": { schema: ApiErrorRef } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
registry.registerPath({ method: "get", path: "/api/v1/users/search", summary: "Search users by name or username", description: "Excludes private accounts and any account you've blocked or that has blocked you. Requires at least 2 characters.", security, request: { query: z.object({ q: z.string().min(2).max(50) }) }, responses: { 200: { description: "Matches (up to 20)", content: { "application/json": { schema: z.object({ users: z.array(UserSearchResultRef) }) } } }, 401: { description: "Unauthorized", content: { "application/json": { schema: ApiErrorRef } } } } });
registry.registerPath({ method: "get", path: "/api/v1/users/{username}", summary: "Get a public profile by username", security: [], request: { params: usernameParam }, responses: { 200: { description: "Profile", content: { "application/json": { schema: UserProfilePublicRef } } }, 404: { description: "Not found", content: { "application/json": { schema: ApiErrorRef } } } } });
registry.registerPath({ method: "post", path: "/api/v1/users/{username}/follow", summary: "Follow a user", description: "Rate-limited: 30 req/min. Fails if either side has blocked the other.", security, request: { params: usernameParam }, responses: { 200: { description: "Following", content: { "application/json": { schema: z.object({ following: z.boolean() }) } } }, 400: { description: "Cannot follow yourself", content: { "application/json": { schema: ApiErrorRef } } }, 404: { description: "Not found", content: { "application/json": { schema: ApiErrorRef } } } } });
diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 61e17d6..efbfe2d 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -766,7 +766,10 @@
"surpriseMe": "Surprise me",
"generatingContent": "Generating recipe content…",
"decrease": "Decrease",
- "increase": "Increase"
+ "increase": "Increase",
+ "skip": "Skip",
+ "continue": "Continue",
+ "finish": "Finish"
},
"auth": {
"signIn": "Sign in",
@@ -1642,5 +1645,33 @@
"previous": "Previous",
"next": "Next",
"pageOf": "Page {page} of {total}"
+ },
+ "onboarding": {
+ "stepOf": "Step {step} of {total}",
+ "welcomeTitle": "Welcome to Epicure",
+ "welcomeBody": "A couple of quick questions to tailor recipes and meal plans to you — takes under a minute, and you can skip any step.",
+ "dietaryTitle": "Dietary preferences & allergens",
+ "dietaryBody": "We'll use these to steer AI recipe generation and flag matching recipes — you can always change them later in Settings.",
+ "dietaryTagsLabel": "Diet",
+ "allergensLabel": "Allergens to avoid",
+ "dietVegan": "Vegan",
+ "dietVegetarian": "Vegetarian",
+ "dietGlutenFree": "Gluten-free",
+ "dietDairyFree": "Dairy-free",
+ "dietNutFree": "Nut-free",
+ "dietHalal": "Halal",
+ "dietKosher": "Kosher",
+ "allergenPeanuts": "Peanuts",
+ "allergenTreeNuts": "Tree nuts",
+ "allergenDairy": "Dairy",
+ "allergenEggs": "Eggs",
+ "allergenShellfish": "Shellfish",
+ "allergenFish": "Fish",
+ "allergenSoy": "Soy",
+ "allergenGluten": "Gluten",
+ "notificationsTitle": "Stay in the loop",
+ "notificationsBody": "Get notified about comments, follows, and shopping list reminders. You can fine-tune categories anytime in Settings → Notifications.",
+ "finishFailed": "Something went wrong — you can finish setup later from Settings",
+ "goToRecipes": "Go to recipes"
}
}
diff --git a/apps/web/messages/fr.json b/apps/web/messages/fr.json
index 53431d7..1950b68 100644
--- a/apps/web/messages/fr.json
+++ b/apps/web/messages/fr.json
@@ -766,7 +766,10 @@
"surpriseMe": "Surprends-moi",
"generatingContent": "Génération du contenu de la recette…",
"decrease": "Diminuer",
- "increase": "Augmenter"
+ "increase": "Augmenter",
+ "skip": "Passer",
+ "continue": "Continuer",
+ "finish": "Terminer"
},
"auth": {
"signIn": "Se connecter",
@@ -1633,5 +1636,33 @@
"previous": "Précédent",
"next": "Suivant",
"pageOf": "Page {page} sur {total}"
+ },
+ "onboarding": {
+ "stepOf": "Étape {step} sur {total}",
+ "welcomeTitle": "Bienvenue sur Epicure",
+ "welcomeBody": "Quelques questions rapides pour adapter les recettes et les plans de repas à vos besoins — moins d'une minute, et chaque étape peut être passée.",
+ "dietaryTitle": "Préférences alimentaires & allergènes",
+ "dietaryBody": "Nous les utiliserons pour orienter la génération de recettes par l'IA et repérer les recettes correspondantes — vous pourrez toujours les modifier plus tard dans les Réglages.",
+ "dietaryTagsLabel": "Régime",
+ "allergensLabel": "Allergènes à éviter",
+ "dietVegan": "Végan",
+ "dietVegetarian": "Végétarien",
+ "dietGlutenFree": "Sans gluten",
+ "dietDairyFree": "Sans lactose",
+ "dietNutFree": "Sans fruits à coque",
+ "dietHalal": "Halal",
+ "dietKosher": "Kasher",
+ "allergenPeanuts": "Arachides",
+ "allergenTreeNuts": "Fruits à coque",
+ "allergenDairy": "Produits laitiers",
+ "allergenEggs": "Œufs",
+ "allergenShellfish": "Crustacés",
+ "allergenFish": "Poisson",
+ "allergenSoy": "Soja",
+ "allergenGluten": "Gluten",
+ "notificationsTitle": "Restez informé",
+ "notificationsBody": "Recevez des notifications pour les commentaires, les abonnements et les rappels de liste de courses. Vous pourrez affiner les catégories à tout moment dans Réglages → Notifications.",
+ "finishFailed": "Une erreur est survenue — vous pourrez terminer la configuration plus tard dans les Réglages",
+ "goToRecipes": "Aller aux recettes"
}
}
diff --git a/apps/web/package.json b/apps/web/package.json
index c55590a..eb65629 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,6 +1,6 @@
{
"name": "@epicure/web",
- "version": "0.76.0",
+ "version": "0.77.0",
"private": true,
"scripts": {
"dev": "next dev",
diff --git a/package.json b/package.json
index dcb82ef..bdcc83d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "epicure",
- "version": "0.76.0",
+ "version": "0.77.0",
"private": true,
"scripts": {
"dev": "pnpm --filter web dev",
diff --git a/packages/db/src/migrations/0064_thick_drax.sql b/packages/db/src/migrations/0064_thick_drax.sql
new file mode 100644
index 0000000..61a0333
--- /dev/null
+++ b/packages/db/src/migrations/0064_thick_drax.sql
@@ -0,0 +1,5 @@
+ALTER TABLE "users" ADD COLUMN "onboarding_completed_at" timestamp;--> statement-breakpoint
+ALTER TABLE "users" ADD COLUMN "dietary_tags" jsonb DEFAULT '{}'::jsonb;--> statement-breakpoint
+-- Backfill existing accounts as already onboarded — the wizard is only for
+-- users who sign up after this migration, not everyone who predates it.
+UPDATE "users" SET "onboarding_completed_at" = "created_at" WHERE "onboarding_completed_at" IS NULL;
\ No newline at end of file
diff --git a/packages/db/src/migrations/meta/0064_snapshot.json b/packages/db/src/migrations/meta/0064_snapshot.json
new file mode 100644
index 0000000..7a37124
--- /dev/null
+++ b/packages/db/src/migrations/meta/0064_snapshot.json
@@ -0,0 +1,6196 @@
+{
+ "id": "d6058ad5-b8d8-43e8-8a95-293dfbe47b4f",
+ "prevId": "da535492-d052-42b6-8b8b-e71e8b3c5320",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "access_token_expires_at": {
+ "name": "access_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_token_expires_at": {
+ "name": "refresh_token_expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password": {
+ "name": "password",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "accounts_user_id_users_id_fk": {
+ "name": "accounts_user_id_users_id_fk",
+ "tableFrom": "accounts",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.api_keys": {
+ "name": "api_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key_hash": {
+ "name": "key_hash",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "scope": {
+ "name": "scope",
+ "type": "api_key_scope",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'full'"
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "api_keys_user_id_users_id_fk": {
+ "name": "api_keys_user_id_users_id_fk",
+ "tableFrom": "api_keys",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "api_keys_key_hash_unique": {
+ "name": "api_keys_key_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "key_hash"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.push_subscriptions": {
+ "name": "push_subscriptions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "endpoint": {
+ "name": "endpoint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "p256dh": {
+ "name": "p256dh",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "auth": {
+ "name": "auth",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "push_subscriptions_user_id_users_id_fk": {
+ "name": "push_subscriptions_user_id_users_id_fk",
+ "tableFrom": "push_subscriptions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "push_subscriptions_endpoint_unique": {
+ "name": "push_subscriptions_endpoint_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "endpoint"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.sessions": {
+ "name": "sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "ip_address": {
+ "name": "ip_address",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_agent": {
+ "name": "user_agent",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "sessions_user_id_users_id_fk": {
+ "name": "sessions_user_id_users_id_fk",
+ "tableFrom": "sessions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "sessions_token_unique": {
+ "name": "sessions_token_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.two_factors": {
+ "name": "two_factors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "backup_codes": {
+ "name": "backup_codes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "verified": {
+ "name": "verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "two_factors_user_id_users_id_fk": {
+ "name": "two_factors_user_id_users_id_fk",
+ "tableFrom": "two_factors",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_ai_keys": {
+ "name": "user_ai_keys",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "encrypted_key": {
+ "name": "encrypted_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_ai_keys_user_id_users_id_fk": {
+ "name": "user_ai_keys_user_id_users_id_fk",
+ "tableFrom": "user_ai_keys",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_blocks": {
+ "name": "user_blocks",
+ "schema": "",
+ "columns": {
+ "blocker_id": {
+ "name": "blocker_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "blocked_id": {
+ "name": "blocked_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_blocks_blocker_id_users_id_fk": {
+ "name": "user_blocks_blocker_id_users_id_fk",
+ "tableFrom": "user_blocks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "blocker_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_blocks_blocked_id_users_id_fk": {
+ "name": "user_blocks_blocked_id_users_id_fk",
+ "tableFrom": "user_blocks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "blocked_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_blocks_blocker_id_blocked_id_pk": {
+ "name": "user_blocks_blocker_id_blocked_id_pk",
+ "columns": [
+ "blocker_id",
+ "blocked_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_feature_prefs": {
+ "name": "user_feature_prefs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "nutrition": {
+ "name": "nutrition",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "pantry": {
+ "name": "pantry",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "meal_plan": {
+ "name": "meal_plan",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "shopping_lists": {
+ "name": "shopping_lists",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "collections": {
+ "name": "collections",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "messages": {
+ "name": "messages",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "chatbots": {
+ "name": "chatbots",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_feature_prefs_user_id_users_id_fk": {
+ "name": "user_feature_prefs_user_id_users_id_fk",
+ "tableFrom": "user_feature_prefs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_feature_prefs_user_id_unique": {
+ "name": "user_feature_prefs_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_follows": {
+ "name": "user_follows",
+ "schema": "",
+ "columns": {
+ "follower_id": {
+ "name": "follower_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "following_id": {
+ "name": "following_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_follows_follower_id_users_id_fk": {
+ "name": "user_follows_follower_id_users_id_fk",
+ "tableFrom": "user_follows",
+ "tableTo": "users",
+ "columnsFrom": [
+ "follower_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_follows_following_id_users_id_fk": {
+ "name": "user_follows_following_id_users_id_fk",
+ "tableFrom": "user_follows",
+ "tableTo": "users",
+ "columnsFrom": [
+ "following_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_follows_follower_id_following_id_pk": {
+ "name": "user_follows_follower_id_following_id_pk",
+ "columns": [
+ "follower_id",
+ "following_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_model_prefs": {
+ "name": "user_model_prefs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text_provider": {
+ "name": "text_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "text_model": {
+ "name": "text_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vision_provider": {
+ "name": "vision_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "vision_model": {
+ "name": "vision_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meal_plan_provider": {
+ "name": "meal_plan_provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meal_plan_model": {
+ "name": "meal_plan_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_model_prefs_user_id_users_id_fk": {
+ "name": "user_model_prefs_user_id_users_id_fk",
+ "tableFrom": "user_model_prefs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_model_prefs_user_id_unique": {
+ "name": "user_model_prefs_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_notification_prefs": {
+ "name": "user_notification_prefs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "follow": {
+ "name": "follow",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "comment": {
+ "name": "comment",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "reply": {
+ "name": "reply",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "reaction": {
+ "name": "reaction",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "rating": {
+ "name": "rating",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "mention": {
+ "name": "mention",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "leftover_expiring": {
+ "name": "leftover_expiring",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "shopping_list": {
+ "name": "shopping_list",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "follow_email": {
+ "name": "follow_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "comment_email": {
+ "name": "comment_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "reply_email": {
+ "name": "reply_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "reaction_email": {
+ "name": "reaction_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "rating_email": {
+ "name": "rating_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "mention_email": {
+ "name": "mention_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "leftover_expiring_email": {
+ "name": "leftover_expiring_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "shopping_list_email": {
+ "name": "shopping_list_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "weekly_digest_email": {
+ "name": "weekly_digest_email",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_notification_prefs_user_id_users_id_fk": {
+ "name": "user_notification_prefs_user_id_users_id_fk",
+ "tableFrom": "user_notification_prefs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_notification_prefs_user_id_unique": {
+ "name": "user_notification_prefs_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_nutrition_goals": {
+ "name": "user_nutrition_goals",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "calories_kcal": {
+ "name": "calories_kcal",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "protein_g": {
+ "name": "protein_g",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "carbs_g": {
+ "name": "carbs_g",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fat_g": {
+ "name": "fat_g",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_nutrition_goals_user_id_users_id_fk": {
+ "name": "user_nutrition_goals_user_id_users_id_fk",
+ "tableFrom": "user_nutrition_goals",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_nutrition_goals_user_id_unique": {
+ "name": "user_nutrition_goals_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email_verified": {
+ "name": "email_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "has_custom_avatar": {
+ "name": "has_custom_avatar",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "avatar_size_mb": {
+ "name": "avatar_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "use_gravatar": {
+ "name": "use_gravatar",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "bio": {
+ "name": "bio",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "private_bio": {
+ "name": "private_bio",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_private": {
+ "name": "is_private",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "username": {
+ "name": "username",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "user_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "tier": {
+ "name": "tier",
+ "type": "tier",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'free'"
+ },
+ "is_developer": {
+ "name": "is_developer",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_byok_enabled": {
+ "name": "is_byok_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "stripe_customer_id": {
+ "name": "stripe_customer_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_subscription_id": {
+ "name": "stripe_subscription_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subscription_status": {
+ "name": "subscription_status",
+ "type": "subscription_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_period_end": {
+ "name": "current_period_end",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit_pref": {
+ "name": "unit_pref",
+ "type": "unit_pref",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'metric'"
+ },
+ "locale": {
+ "name": "locale",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'en'"
+ },
+ "two_factor_enabled": {
+ "name": "two_factor_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "onboarding_completed_at": {
+ "name": "onboarding_completed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dietary_tags": {
+ "name": "dietary_tags",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "email"
+ ]
+ },
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "username"
+ ]
+ },
+ "users_stripe_customer_id_unique": {
+ "name": "users_stripe_customer_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "stripe_customer_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.verifications": {
+ "name": "verifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "identifier": {
+ "name": "identifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ingredients": {
+ "name": "ingredients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "aliases": {
+ "name": "aliases",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "known_allergens": {
+ "name": "known_allergens",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "ingredients_name_unique": {
+ "name": "ingredients_name_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "name"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_batch_dishes": {
+ "name": "recipe_batch_dishes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "fridge_days": {
+ "name": "fridge_days",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "freezer_friendly": {
+ "name": "freezer_friendly",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "freezer_note": {
+ "name": "freezer_note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "day_of_instructions": {
+ "name": "day_of_instructions",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "recipe_batch_dishes_recipe_idx": {
+ "name": "recipe_batch_dishes_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "recipe_batch_dishes_recipe_id_recipes_id_fk": {
+ "name": "recipe_batch_dishes_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_batch_dishes",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_ingredients": {
+ "name": "recipe_ingredients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ingredient_id": {
+ "name": "ingredient_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw_name": {
+ "name": "raw_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric(10, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "recipe_ingredients_recipe_idx": {
+ "name": "recipe_ingredients_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "recipe_ingredients_recipe_id_recipes_id_fk": {
+ "name": "recipe_ingredients_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_ingredients",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "recipe_ingredients_ingredient_id_ingredients_id_fk": {
+ "name": "recipe_ingredients_ingredient_id_ingredients_id_fk",
+ "tableFrom": "recipe_ingredients",
+ "tableTo": "ingredients",
+ "columnsFrom": [
+ "ingredient_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_notes": {
+ "name": "recipe_notes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "recipe_notes_recipe_user_idx": {
+ "name": "recipe_notes_recipe_user_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "recipe_notes_recipe_id_recipes_id_fk": {
+ "name": "recipe_notes_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_notes",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "recipe_notes_user_id_users_id_fk": {
+ "name": "recipe_notes_user_id_users_id_fk",
+ "tableFrom": "recipe_notes",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_photos": {
+ "name": "recipe_photos",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "is_cover": {
+ "name": "is_cover",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "size_mb": {
+ "name": "size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "recipe_photos_recipe_id_recipes_id_fk": {
+ "name": "recipe_photos_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_photos",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_snapshots": {
+ "name": "recipe_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "snapshot_data": {
+ "name": "snapshot_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "recipe_snapshots_recipe_idx": {
+ "name": "recipe_snapshots_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "recipe_snapshots_recipe_id_recipes_id_fk": {
+ "name": "recipe_snapshots_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_snapshots",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "recipe_snapshots_author_id_users_id_fk": {
+ "name": "recipe_snapshots_author_id_users_id_fk",
+ "tableFrom": "recipe_snapshots",
+ "tableTo": "users",
+ "columnsFrom": [
+ "author_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_steps": {
+ "name": "recipe_steps",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "order": {
+ "name": "order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "instruction": {
+ "name": "instruction",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "timer_seconds": {
+ "name": "timer_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "photo_url": {
+ "name": "photo_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "applies_to": {
+ "name": "applies_to",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ }
+ },
+ "indexes": {
+ "recipe_steps_recipe_idx": {
+ "name": "recipe_steps_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "recipe_steps_recipe_id_recipes_id_fk": {
+ "name": "recipe_steps_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_steps",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipe_variations": {
+ "name": "recipe_variations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "parent_recipe_id": {
+ "name": "parent_recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "child_recipe_id": {
+ "name": "child_recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ai_generated": {
+ "name": "ai_generated",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "recipe_variations_parent_recipe_id_recipes_id_fk": {
+ "name": "recipe_variations_parent_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_variations",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "parent_recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "recipe_variations_child_recipe_id_recipes_id_fk": {
+ "name": "recipe_variations_child_recipe_id_recipes_id_fk",
+ "tableFrom": "recipe_variations",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "child_recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.recipes": {
+ "name": "recipes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "base_servings": {
+ "name": "base_servings",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 4
+ },
+ "recipe_type": {
+ "name": "recipe_type",
+ "type": "recipe_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'dish'"
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "visibility",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'private'"
+ },
+ "source_url": {
+ "name": "source_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ai_generated": {
+ "name": "ai_generated",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "ai_model": {
+ "name": "ai_model",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ai_prompt": {
+ "name": "ai_prompt",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dietary_tags": {
+ "name": "dietary_tags",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false,
+ "default": "'{}'::jsonb"
+ },
+ "dietary_verified": {
+ "name": "dietary_verified",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "nutrition_data": {
+ "name": "nutrition_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "nutrition_manual": {
+ "name": "nutrition_manual",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "difficulty": {
+ "name": "difficulty",
+ "type": "difficulty",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "prep_mins": {
+ "name": "prep_mins",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cook_mins": {
+ "name": "cook_mins",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "is_batch_cook": {
+ "name": "is_batch_cook",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "cover_icon": {
+ "name": "cover_icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_color": {
+ "name": "cover_color",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "recipes_author_idx": {
+ "name": "recipes_author_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "recipes_visibility_idx": {
+ "name": "recipes_visibility_idx",
+ "columns": [
+ {
+ "expression": "visibility",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "recipes_dietary_tags_gin": {
+ "name": "recipes_dietary_tags_gin",
+ "columns": [
+ {
+ "expression": "dietary_tags",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "recipes_batch_cook_idx": {
+ "name": "recipes_batch_cook_idx",
+ "columns": [
+ {
+ "expression": "is_batch_cook",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "recipes_type_idx": {
+ "name": "recipes_type_idx",
+ "columns": [
+ {
+ "expression": "recipe_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "recipes_author_id_users_id_fk": {
+ "name": "recipes_author_id_users_id_fk",
+ "tableFrom": "recipes",
+ "tableTo": "users",
+ "columnsFrom": [
+ "author_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_allergens": {
+ "name": "user_allergens",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "allergen_tag": {
+ "name": "allergen_tag",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_allergens_user_id_users_id_fk": {
+ "name": "user_allergens_user_id_users_id_fk",
+ "tableFrom": "user_allergens",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collection_favorites": {
+ "name": "collection_favorites",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "collection_id": {
+ "name": "collection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collection_favorites_collection_idx": {
+ "name": "collection_favorites_collection_idx",
+ "columns": [
+ {
+ "expression": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "collection_favorites_user_collection_idx": {
+ "name": "collection_favorites_user_collection_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collection_favorites_user_id_users_id_fk": {
+ "name": "collection_favorites_user_id_users_id_fk",
+ "tableFrom": "collection_favorites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "collection_favorites_collection_id_collections_id_fk": {
+ "name": "collection_favorites_collection_id_collections_id_fk",
+ "tableFrom": "collection_favorites",
+ "tableTo": "collections",
+ "columnsFrom": [
+ "collection_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collection_members": {
+ "name": "collection_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "collection_id": {
+ "name": "collection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "collection_member_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'viewer'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collection_members_user_idx": {
+ "name": "collection_members_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collection_members_collection_id_collections_id_fk": {
+ "name": "collection_members_collection_id_collections_id_fk",
+ "tableFrom": "collection_members",
+ "tableTo": "collections",
+ "columnsFrom": [
+ "collection_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "collection_members_user_id_users_id_fk": {
+ "name": "collection_members_user_id_users_id_fk",
+ "tableFrom": "collection_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collection_recipes": {
+ "name": "collection_recipes",
+ "schema": "",
+ "columns": {
+ "collection_id": {
+ "name": "collection_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collection_recipes_collection_position_idx": {
+ "name": "collection_recipes_collection_position_idx",
+ "columns": [
+ {
+ "expression": "collection_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collection_recipes_collection_id_collections_id_fk": {
+ "name": "collection_recipes_collection_id_collections_id_fk",
+ "tableFrom": "collection_recipes",
+ "tableTo": "collections",
+ "columnsFrom": [
+ "collection_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "collection_recipes_recipe_id_recipes_id_fk": {
+ "name": "collection_recipes_recipe_id_recipes_id_fk",
+ "tableFrom": "collection_recipes",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collections": {
+ "name": "collections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tags": {
+ "name": "tags",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "visibility": {
+ "name": "visibility",
+ "type": "visibility",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'private'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collections_user_idx": {
+ "name": "collections_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collections_user_id_users_id_fk": {
+ "name": "collections_user_id_users_id_fk",
+ "tableFrom": "collections",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.comment_reactions": {
+ "name": "comment_reactions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "comment_reaction_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "comment_reactions_comment_idx": {
+ "name": "comment_reactions_comment_idx",
+ "columns": [
+ {
+ "expression": "comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "comment_reactions_comment_id_comments_id_fk": {
+ "name": "comment_reactions_comment_id_comments_id_fk",
+ "tableFrom": "comment_reactions",
+ "tableTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comment_reactions_user_id_users_id_fk": {
+ "name": "comment_reactions_user_id_users_id_fk",
+ "tableFrom": "comment_reactions",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.comments": {
+ "name": "comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "comments_recipe_idx": {
+ "name": "comments_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "comments_recipe_id_recipes_id_fk": {
+ "name": "comments_recipe_id_recipes_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comments_user_id_users_id_fk": {
+ "name": "comments_user_id_users_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "comments_parent_id_comments_id_fk": {
+ "name": "comments_parent_id_comments_id_fk",
+ "tableFrom": "comments",
+ "tableTo": "comments",
+ "columnsFrom": [
+ "parent_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.cooking_history": {
+ "name": "cooking_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "batch_dish_id": {
+ "name": "batch_dish_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cooked_at": {
+ "name": "cooked_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "servings": {
+ "name": "servings",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "notes": {
+ "name": "notes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expiry_reminder_sent_at": {
+ "name": "expiry_reminder_sent_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "cooking_history_user_idx": {
+ "name": "cooking_history_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "cooking_history_batch_dish_idx": {
+ "name": "cooking_history_batch_dish_idx",
+ "columns": [
+ {
+ "expression": "batch_dish_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "cooking_history_user_id_users_id_fk": {
+ "name": "cooking_history_user_id_users_id_fk",
+ "tableFrom": "cooking_history",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "cooking_history_recipe_id_recipes_id_fk": {
+ "name": "cooking_history_recipe_id_recipes_id_fk",
+ "tableFrom": "cooking_history",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "cooking_history_batch_dish_id_recipe_batch_dishes_id_fk": {
+ "name": "cooking_history_batch_dish_id_recipe_batch_dishes_id_fk",
+ "tableFrom": "cooking_history",
+ "tableTo": "recipe_batch_dishes",
+ "columnsFrom": [
+ "batch_dish_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.favorites": {
+ "name": "favorites",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "favorites_user_idx": {
+ "name": "favorites_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "favorites_user_id_users_id_fk": {
+ "name": "favorites_user_id_users_id_fk",
+ "tableFrom": "favorites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "favorites_recipe_id_recipes_id_fk": {
+ "name": "favorites_recipe_id_recipes_id_fk",
+ "tableFrom": "favorites",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.notifications": {
+ "name": "notifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "notification_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "actor_id": {
+ "name": "actor_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "read": {
+ "name": "read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "notifications_user_idx": {
+ "name": "notifications_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_user_unread_idx": {
+ "name": "notifications_user_unread_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notifications_user_id_users_id_fk": {
+ "name": "notifications_user_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_actor_id_users_id_fk": {
+ "name": "notifications_actor_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": [
+ "actor_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_recipe_id_recipes_id_fk": {
+ "name": "notifications_recipe_id_recipes_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "notifications_comment_id_comments_id_fk": {
+ "name": "notifications_comment_id_comments_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "comments",
+ "columnsFrom": [
+ "comment_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ratings": {
+ "name": "ratings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "score": {
+ "name": "score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "review_text": {
+ "name": "review_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "photo_key": {
+ "name": "photo_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "photo_size_mb": {
+ "name": "photo_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ratings_user_idx": {
+ "name": "ratings_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ratings_recipe_idx": {
+ "name": "ratings_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ratings_recipe_id_recipes_id_fk": {
+ "name": "ratings_recipe_id_recipes_id_fk",
+ "tableFrom": "ratings",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "ratings_user_id_users_id_fk": {
+ "name": "ratings_user_id_users_id_fk",
+ "tableFrom": "ratings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.meal_plan_entries": {
+ "name": "meal_plan_entries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "meal_plan_id": {
+ "name": "meal_plan_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "weekday",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "meal_type": {
+ "name": "meal_type",
+ "type": "meal_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "batch_dish_id": {
+ "name": "batch_dish_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "servings": {
+ "name": "servings",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 2
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "meal_plan_entries_plan_idx": {
+ "name": "meal_plan_entries_plan_idx",
+ "columns": [
+ {
+ "expression": "meal_plan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "meal_plan_entries_plan_day_meal_idx": {
+ "name": "meal_plan_entries_plan_day_meal_idx",
+ "columns": [
+ {
+ "expression": "meal_plan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "meal_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "meal_plan_entries_meal_plan_id_meal_plans_id_fk": {
+ "name": "meal_plan_entries_meal_plan_id_meal_plans_id_fk",
+ "tableFrom": "meal_plan_entries",
+ "tableTo": "meal_plans",
+ "columnsFrom": [
+ "meal_plan_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "meal_plan_entries_recipe_id_recipes_id_fk": {
+ "name": "meal_plan_entries_recipe_id_recipes_id_fk",
+ "tableFrom": "meal_plan_entries",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "meal_plan_entries_batch_dish_id_recipe_batch_dishes_id_fk": {
+ "name": "meal_plan_entries_batch_dish_id_recipe_batch_dishes_id_fk",
+ "tableFrom": "meal_plan_entries",
+ "tableTo": "recipe_batch_dishes",
+ "columnsFrom": [
+ "batch_dish_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.meal_plan_members": {
+ "name": "meal_plan_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "meal_plan_id": {
+ "name": "meal_plan_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "collection_member_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'viewer'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "meal_plan_members_plan_idx": {
+ "name": "meal_plan_members_plan_idx",
+ "columns": [
+ {
+ "expression": "meal_plan_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "meal_plan_members_user_idx": {
+ "name": "meal_plan_members_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "meal_plan_members_meal_plan_id_meal_plans_id_fk": {
+ "name": "meal_plan_members_meal_plan_id_meal_plans_id_fk",
+ "tableFrom": "meal_plan_members",
+ "tableTo": "meal_plans",
+ "columnsFrom": [
+ "meal_plan_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "meal_plan_members_user_id_users_id_fk": {
+ "name": "meal_plan_members_user_id_users_id_fk",
+ "tableFrom": "meal_plan_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.meal_plans": {
+ "name": "meal_plans",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "week_start": {
+ "name": "week_start",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "meal_plans_user_idx": {
+ "name": "meal_plans_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "meal_plans_user_week_idx": {
+ "name": "meal_plans_user_week_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "week_start",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "meal_plans_user_id_users_id_fk": {
+ "name": "meal_plans_user_id_users_id_fk",
+ "tableFrom": "meal_plans",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.pantry_items": {
+ "name": "pantry_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ingredient_id": {
+ "name": "ingredient_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw_name": {
+ "name": "raw_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric(10, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "pantry_items_user_idx": {
+ "name": "pantry_items_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "pantry_items_user_id_users_id_fk": {
+ "name": "pantry_items_user_id_users_id_fk",
+ "tableFrom": "pantry_items",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "pantry_items_ingredient_id_ingredients_id_fk": {
+ "name": "pantry_items_ingredient_id_ingredients_id_fk",
+ "tableFrom": "pantry_items",
+ "tableTo": "ingredients",
+ "columnsFrom": [
+ "ingredient_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.shopping_list_items": {
+ "name": "shopping_list_items",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "list_id": {
+ "name": "list_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ingredient_id": {
+ "name": "ingredient_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "raw_name": {
+ "name": "raw_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "quantity": {
+ "name": "quantity",
+ "type": "numeric(10, 4)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "unit": {
+ "name": "unit",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "aisle": {
+ "name": "aisle",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "checked": {
+ "name": "checked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "in_pantry": {
+ "name": "in_pantry",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "shopping_list_items_list_id_shopping_lists_id_fk": {
+ "name": "shopping_list_items_list_id_shopping_lists_id_fk",
+ "tableFrom": "shopping_list_items",
+ "tableTo": "shopping_lists",
+ "columnsFrom": [
+ "list_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "shopping_list_items_ingredient_id_ingredients_id_fk": {
+ "name": "shopping_list_items_ingredient_id_ingredients_id_fk",
+ "tableFrom": "shopping_list_items",
+ "tableTo": "ingredients",
+ "columnsFrom": [
+ "ingredient_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.shopping_list_members": {
+ "name": "shopping_list_members",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "list_id": {
+ "name": "list_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "role": {
+ "name": "role",
+ "type": "collection_member_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'viewer'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "shopping_list_members_list_idx": {
+ "name": "shopping_list_members_list_idx",
+ "columns": [
+ {
+ "expression": "list_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "shopping_list_members_user_idx": {
+ "name": "shopping_list_members_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "shopping_list_members_list_id_shopping_lists_id_fk": {
+ "name": "shopping_list_members_list_id_shopping_lists_id_fk",
+ "tableFrom": "shopping_list_members",
+ "tableTo": "shopping_lists",
+ "columnsFrom": [
+ "list_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "shopping_list_members_user_id_users_id_fk": {
+ "name": "shopping_list_members_user_id_users_id_fk",
+ "tableFrom": "shopping_list_members",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.shopping_lists": {
+ "name": "shopping_lists",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "public_editable": {
+ "name": "public_editable",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "generated_at": {
+ "name": "generated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "shopping_lists_user_id_users_id_fk": {
+ "name": "shopping_lists_user_id_users_id_fk",
+ "tableFrom": "shopping_lists",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.audit_logs": {
+ "name": "audit_logs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_type": {
+ "name": "target_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata": {
+ "name": "metadata",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "audit_logs_created_idx": {
+ "name": "audit_logs_created_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_logs_user_id_users_id_fk": {
+ "name": "audit_logs_user_id_users_id_fk",
+ "tableFrom": "audit_logs",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.invites": {
+ "name": "invites",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "user_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'user'"
+ },
+ "tier": {
+ "name": "tier",
+ "type": "tier",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'free'"
+ },
+ "created_by_id": {
+ "name": "created_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "used_by_id": {
+ "name": "used_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "invites_created_by_id_users_id_fk": {
+ "name": "invites_created_by_id_users_id_fk",
+ "tableFrom": "invites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "invites_used_by_id_users_id_fk": {
+ "name": "invites_used_by_id_users_id_fk",
+ "tableFrom": "invites",
+ "tableTo": "users",
+ "columnsFrom": [
+ "used_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "invites_token_uniq": {
+ "name": "invites_token_uniq",
+ "nullsNotDistinct": false,
+ "columns": [
+ "token"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reports": {
+ "name": "reports",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "reporter_id": {
+ "name": "reporter_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_type": {
+ "name": "target_type",
+ "type": "report_target_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "target_id": {
+ "name": "target_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "report_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "reviewed_by_id": {
+ "name": "reviewed_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "reviewed_at": {
+ "name": "reviewed_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "reports_status_idx": {
+ "name": "reports_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reports_reporter_id_users_id_fk": {
+ "name": "reports_reporter_id_users_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reporter_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reports_reviewed_by_id_users_id_fk": {
+ "name": "reports_reviewed_by_id_users_id_fk",
+ "tableFrom": "reports",
+ "tableTo": "users",
+ "columnsFrom": [
+ "reviewed_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.site_settings": {
+ "name": "site_settings",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "is_secret": {
+ "name": "is_secret",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_by_id": {
+ "name": "updated_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "site_settings_updated_by_id_users_id_fk": {
+ "name": "site_settings_updated_by_id_users_id_fk",
+ "tableFrom": "site_settings",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tier_definitions": {
+ "name": "tier_definitions",
+ "schema": "",
+ "columns": {
+ "tier": {
+ "name": "tier",
+ "type": "tier",
+ "typeSchema": "public",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "max_recipes": {
+ "name": "max_recipes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ai_calls_per_month": {
+ "name": "ai_calls_per_month",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_mb": {
+ "name": "storage_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "max_public_recipes": {
+ "name": "max_public_recipes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stripe_product_id": {
+ "name": "stripe_product_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_price_id_monthly": {
+ "name": "stripe_price_id_monthly",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "stripe_price_id_yearly": {
+ "name": "stripe_price_id_yearly",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_usage": {
+ "name": "user_usage",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "month": {
+ "name": "month",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ai_calls_used": {
+ "name": "ai_calls_used",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "user_usage_user_month_idx": {
+ "name": "user_usage_user_month_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "month",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_usage_user_id_users_id_fk": {
+ "name": "user_usage_user_id_users_id_fk",
+ "tableFrom": "user_usage",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_usage_user_month_uniq": {
+ "name": "user_usage_user_month_uniq",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id",
+ "month"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.admin_webhook_deliveries": {
+ "name": "admin_webhook_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhook_id": {
+ "name": "webhook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event": {
+ "name": "event",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_code": {
+ "name": "status_code",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "success": {
+ "name": "success",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "admin_webhook_deliveries_webhook_id_admin_webhooks_id_fk": {
+ "name": "admin_webhook_deliveries_webhook_id_admin_webhooks_id_fk",
+ "tableFrom": "admin_webhook_deliveries",
+ "tableTo": "admin_webhooks",
+ "columnsFrom": [
+ "webhook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.admin_webhooks": {
+ "name": "admin_webhooks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_by_id": {
+ "name": "created_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "events": {
+ "name": "events",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "admin_webhooks_created_by_id_users_id_fk": {
+ "name": "admin_webhooks_created_by_id_users_id_fk",
+ "tableFrom": "admin_webhooks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "created_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhook_deliveries": {
+ "name": "webhook_deliveries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "webhook_id": {
+ "name": "webhook_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "event": {
+ "name": "event",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "payload": {
+ "name": "payload",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_code": {
+ "name": "status_code",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "success": {
+ "name": "success",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "attempts": {
+ "name": "attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "webhook_deliveries_webhook_id_webhooks_id_fk": {
+ "name": "webhook_deliveries_webhook_id_webhooks_id_fk",
+ "tableFrom": "webhook_deliveries",
+ "tableTo": "webhooks",
+ "columnsFrom": [
+ "webhook_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.webhooks": {
+ "name": "webhooks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "url": {
+ "name": "url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "events": {
+ "name": "events",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'"
+ },
+ "secret": {
+ "name": "secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "webhooks_user_id_users_id_fk": {
+ "name": "webhooks_user_id_users_id_fk",
+ "tableFrom": "webhooks",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.conversation_reads": {
+ "name": "conversation_reads",
+ "schema": "",
+ "columns": {
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_read_at": {
+ "name": "last_read_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "conversation_reads_conversation_id_conversations_id_fk": {
+ "name": "conversation_reads_conversation_id_conversations_id_fk",
+ "tableFrom": "conversation_reads",
+ "tableTo": "conversations",
+ "columnsFrom": [
+ "conversation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "conversation_reads_user_id_users_id_fk": {
+ "name": "conversation_reads_user_id_users_id_fk",
+ "tableFrom": "conversation_reads",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "conversation_reads_conversation_id_user_id_pk": {
+ "name": "conversation_reads_conversation_id_user_id_pk",
+ "columns": [
+ "conversation_id",
+ "user_id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.conversations": {
+ "name": "conversations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_a_id": {
+ "name": "user_a_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_b_id": {
+ "name": "user_b_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_message_at": {
+ "name": "last_message_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "conversations_last_message_idx": {
+ "name": "conversations_last_message_idx",
+ "columns": [
+ {
+ "expression": "last_message_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "conversations_user_a_id_users_id_fk": {
+ "name": "conversations_user_a_id_users_id_fk",
+ "tableFrom": "conversations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_a_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "conversations_user_b_id_users_id_fk": {
+ "name": "conversations_user_b_id_users_id_fk",
+ "tableFrom": "conversations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_b_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "conversations_pair_uniq": {
+ "name": "conversations_pair_uniq",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_a_id",
+ "user_b_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.messages": {
+ "name": "messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sender_id": {
+ "name": "sender_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "messages_conversation_idx": {
+ "name": "messages_conversation_idx",
+ "columns": [
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "messages_conversation_id_conversations_id_fk": {
+ "name": "messages_conversation_id_conversations_id_fk",
+ "tableFrom": "messages",
+ "tableTo": "conversations",
+ "columnsFrom": [
+ "conversation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "messages_sender_id_users_id_fk": {
+ "name": "messages_sender_id_users_id_fk",
+ "tableFrom": "messages",
+ "tableTo": "users",
+ "columnsFrom": [
+ "sender_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.processed_stripe_events": {
+ "name": "processed_stripe_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_billing_details": {
+ "name": "user_billing_details",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "full_name": {
+ "name": "full_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_line1": {
+ "name": "address_line1",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "address_line2": {
+ "name": "address_line2",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "city": {
+ "name": "city",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "postal_code": {
+ "name": "postal_code",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "country": {
+ "name": "country",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "phone": {
+ "name": "phone",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_billing_details_user_id_users_id_fk": {
+ "name": "user_billing_details_user_id_users_id_fk",
+ "tableFrom": "user_billing_details",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "user_billing_details_user_id_unique": {
+ "name": "user_billing_details_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": [
+ "user_id"
+ ]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.ai_conversations": {
+ "name": "ai_conversations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ai_conversations_user_idx": {
+ "name": "ai_conversations_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "ai_conversations_user_id_users_id_fk": {
+ "name": "ai_conversations_user_id_users_id_fk",
+ "tableFrom": "ai_conversations",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.chat_messages": {
+ "name": "chat_messages",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipe_id": {
+ "name": "recipe_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "conversation_id": {
+ "name": "conversation_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "chat_role",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "chat_messages_user_idx": {
+ "name": "chat_messages_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_messages_recipe_idx": {
+ "name": "chat_messages_recipe_idx",
+ "columns": [
+ {
+ "expression": "recipe_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "chat_messages_conversation_idx": {
+ "name": "chat_messages_conversation_idx",
+ "columns": [
+ {
+ "expression": "conversation_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "chat_messages_user_id_users_id_fk": {
+ "name": "chat_messages_user_id_users_id_fk",
+ "tableFrom": "chat_messages",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_messages_recipe_id_recipes_id_fk": {
+ "name": "chat_messages_recipe_id_recipes_id_fk",
+ "tableFrom": "chat_messages",
+ "tableTo": "recipes",
+ "columnsFrom": [
+ "recipe_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "chat_messages_conversation_id_ai_conversations_id_fk": {
+ "name": "chat_messages_conversation_id_ai_conversations_id_fk",
+ "tableFrom": "chat_messages",
+ "tableTo": "ai_conversations",
+ "columnsFrom": [
+ "conversation_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.processed_gitea_events": {
+ "name": "processed_gitea_events",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.support_ticket_attachments": {
+ "name": "support_ticket_attachments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "ticket_id": {
+ "name": "ticket_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "storage_key": {
+ "name": "storage_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "content_type": {
+ "name": "content_type",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "support_ticket_attachments_ticket_idx": {
+ "name": "support_ticket_attachments_ticket_idx",
+ "columns": [
+ {
+ "expression": "ticket_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "support_ticket_attachments_ticket_id_support_tickets_id_fk": {
+ "name": "support_ticket_attachments_ticket_id_support_tickets_id_fk",
+ "tableFrom": "support_ticket_attachments",
+ "tableTo": "support_tickets",
+ "columnsFrom": [
+ "ticket_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.support_ticket_comments": {
+ "name": "support_ticket_comments",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "ticket_id": {
+ "name": "ticket_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_type": {
+ "name": "author_type",
+ "type": "support_comment_author_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "body": {
+ "name": "body",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "gitea_comment_id": {
+ "name": "gitea_comment_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "support_ticket_comments_ticket_idx": {
+ "name": "support_ticket_comments_ticket_idx",
+ "columns": [
+ {
+ "expression": "ticket_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "support_ticket_comments_gitea_comment_idx": {
+ "name": "support_ticket_comments_gitea_comment_idx",
+ "columns": [
+ {
+ "expression": "gitea_comment_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "support_ticket_comments_ticket_id_support_tickets_id_fk": {
+ "name": "support_ticket_comments_ticket_id_support_tickets_id_fk",
+ "tableFrom": "support_ticket_comments",
+ "tableTo": "support_tickets",
+ "columnsFrom": [
+ "ticket_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.support_tickets": {
+ "name": "support_tickets",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "support_ticket_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "support_ticket_status",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'open'"
+ },
+ "gitea_issue_url": {
+ "name": "gitea_issue_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitea_issue_number": {
+ "name": "gitea_issue_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "gitea_error": {
+ "name": "gitea_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "support_tickets_user_idx": {
+ "name": "support_tickets_user_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "support_tickets_status_idx": {
+ "name": "support_tickets_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "support_tickets_gitea_issue_idx": {
+ "name": "support_tickets_gitea_issue_idx",
+ "columns": [
+ {
+ "expression": "gitea_issue_number",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "support_tickets_user_id_users_id_fk": {
+ "name": "support_tickets_user_id_users_id_fk",
+ "tableFrom": "support_tickets",
+ "tableTo": "users",
+ "columnsFrom": [
+ "user_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.feature_flags": {
+ "name": "feature_flags",
+ "schema": "",
+ "columns": {
+ "feature_key": {
+ "name": "feature_key",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tier": {
+ "name": "tier",
+ "type": "tier",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_by_id": {
+ "name": "updated_by_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "feature_flags_updated_by_id_users_id_fk": {
+ "name": "feature_flags_updated_by_id_users_id_fk",
+ "tableFrom": "feature_flags",
+ "tableTo": "users",
+ "columnsFrom": [
+ "updated_by_id"
+ ],
+ "columnsTo": [
+ "id"
+ ],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "feature_flags_feature_key_tier_pk": {
+ "name": "feature_flags_feature_key_tier_pk",
+ "columns": [
+ "feature_key",
+ "tier"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.api_key_scope": {
+ "name": "api_key_scope",
+ "schema": "public",
+ "values": [
+ "full",
+ "read"
+ ]
+ },
+ "public.subscription_status": {
+ "name": "subscription_status",
+ "schema": "public",
+ "values": [
+ "active",
+ "trialing",
+ "past_due",
+ "canceled",
+ "incomplete"
+ ]
+ },
+ "public.tier": {
+ "name": "tier",
+ "schema": "public",
+ "values": [
+ "free",
+ "pro",
+ "family"
+ ]
+ },
+ "public.unit_pref": {
+ "name": "unit_pref",
+ "schema": "public",
+ "values": [
+ "metric",
+ "imperial"
+ ]
+ },
+ "public.user_role": {
+ "name": "user_role",
+ "schema": "public",
+ "values": [
+ "user",
+ "moderator",
+ "admin"
+ ]
+ },
+ "public.difficulty": {
+ "name": "difficulty",
+ "schema": "public",
+ "values": [
+ "easy",
+ "medium",
+ "hard"
+ ]
+ },
+ "public.recipe_type": {
+ "name": "recipe_type",
+ "schema": "public",
+ "values": [
+ "dish",
+ "drink"
+ ]
+ },
+ "public.visibility": {
+ "name": "visibility",
+ "schema": "public",
+ "values": [
+ "private",
+ "unlisted",
+ "public",
+ "followers"
+ ]
+ },
+ "public.collection_member_role": {
+ "name": "collection_member_role",
+ "schema": "public",
+ "values": [
+ "viewer",
+ "editor"
+ ]
+ },
+ "public.comment_reaction_type": {
+ "name": "comment_reaction_type",
+ "schema": "public",
+ "values": [
+ "like",
+ "love",
+ "laugh",
+ "wow",
+ "sad",
+ "fire"
+ ]
+ },
+ "public.notification_type": {
+ "name": "notification_type",
+ "schema": "public",
+ "values": [
+ "follow",
+ "comment",
+ "reply",
+ "reaction",
+ "rating",
+ "mention"
+ ]
+ },
+ "public.meal_type": {
+ "name": "meal_type",
+ "schema": "public",
+ "values": [
+ "breakfast",
+ "lunch",
+ "dinner",
+ "snack"
+ ]
+ },
+ "public.weekday": {
+ "name": "weekday",
+ "schema": "public",
+ "values": [
+ "mon",
+ "tue",
+ "wed",
+ "thu",
+ "fri",
+ "sat",
+ "sun"
+ ]
+ },
+ "public.report_status": {
+ "name": "report_status",
+ "schema": "public",
+ "values": [
+ "pending",
+ "reviewed",
+ "dismissed"
+ ]
+ },
+ "public.report_target_type": {
+ "name": "report_target_type",
+ "schema": "public",
+ "values": [
+ "recipe",
+ "comment",
+ "user"
+ ]
+ },
+ "public.chat_role": {
+ "name": "chat_role",
+ "schema": "public",
+ "values": [
+ "user",
+ "assistant"
+ ]
+ },
+ "public.support_comment_author_type": {
+ "name": "support_comment_author_type",
+ "schema": "public",
+ "values": [
+ "user",
+ "admin",
+ "gitea"
+ ]
+ },
+ "public.support_ticket_status": {
+ "name": "support_ticket_status",
+ "schema": "public",
+ "values": [
+ "open",
+ "triaged",
+ "closed"
+ ]
+ },
+ "public.support_ticket_type": {
+ "name": "support_ticket_type",
+ "schema": "public",
+ "values": [
+ "bug",
+ "suggestion",
+ "question"
+ ]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
\ No newline at end of file
diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json
index fc939ce..a5bb3d6 100644
--- a/packages/db/src/migrations/meta/_journal.json
+++ b/packages/db/src/migrations/meta/_journal.json
@@ -449,6 +449,13 @@
"when": 1784885406472,
"tag": "0063_sharp_stone_men",
"breakpoints": true
+ },
+ {
+ "idx": 64,
+ "version": "7",
+ "when": 1784885885146,
+ "tag": "0064_thick_drax",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/packages/db/src/schema/users.ts b/packages/db/src/schema/users.ts
index ab317cf..c365e8d 100644
--- a/packages/db/src/schema/users.ts
+++ b/packages/db/src/schema/users.ts
@@ -4,11 +4,25 @@ import {
timestamp,
boolean,
integer,
+ jsonb,
pgEnum,
primaryKey,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
+// Mirrors recipes.ts's DietaryTags shape — duplicated rather than imported to
+// avoid a users.ts <-> recipes.ts circular import (recipes.ts already
+// references users.ts for the authorId FK).
+export type UserDietaryTags = {
+ vegan?: boolean;
+ vegetarian?: boolean;
+ glutenFree?: boolean;
+ dairyFree?: boolean;
+ nutFree?: boolean;
+ halal?: boolean;
+ kosher?: boolean;
+};
+
export const userRoleEnum = pgEnum("user_role", ["user", "moderator", "admin"]);
export const tierEnum = pgEnum("tier", ["free", "pro", "family"]);
export const unitPrefEnum = pgEnum("unit_pref", ["metric", "imperial"]);
@@ -55,6 +69,11 @@ export const users = pgTable("users", {
unitPref: unitPrefEnum("unit_pref").notNull().default("metric"),
locale: text("locale").notNull().default("en"),
twoFactorEnabled: boolean("two_factor_enabled").notNull().default(false),
+ // Null = hasn't been through (or finished/skipped) the post-signup
+ // onboarding wizard yet. Set once, either on completion or on skip — both
+ // count as "done," so the wizard never shows twice.
+ onboardingCompletedAt: timestamp("onboarding_completed_at"),
+ dietaryTags: jsonb("dietary_tags").$type().default({}),
createdAt: timestamp("created_at").notNull().defaultNow(),
updatedAt: timestamp("updated_at").notNull().defaultNow(),
});