Skip to main content
RapidDev - Software Development Agency
Lovable PromptsB2C SaaSBeginner

Lovable Prompts for Building a Recipe App

A visual recipe catalog with normalized ingredients and steps, dietary tag filtering, a servings scaler, favorites for signed-in users, and optional recipe submission — built on Lovable Cloud with public-read RLS.

Time to MVP

~2-3 hours

Credits

~40-80 credits for full chain

Difficulty

Beginner

Cloud features

3

TL;DR

The one-paragraph version before you dive in.

This prompt kit builds a food-forward recipe app where users browse a public recipe catalog with photos, view ingredients and step-by-step instructions, save favorites, filter by dietary tags, and optionally submit their own recipes — all backed by Lovable Cloud. The key architectural decision: model ingredients and steps as separate normalized tables with a position column, not as a text blob. That one choice unlocks servings scaling, shopping lists, and dietary filters later.

Setup checklist

Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.

Cloud tab settings

Database

Stores recipes, ingredients (separate table with position), steps (separate table with position), favorites, categories, and profiles. The normalized schema is the foundation for servings scaling and shopping lists.

  1. 1Click the + icon next to the preview panel, then click 'Cloud tab' → 'Database'.
  2. 2Lovable Cloud provisions Supabase Postgres automatically.
  3. 3The starter prompt generates the migration. If it does not auto-run, paste the migration SQL into Cloud → Database → SQL Editor.
  4. 4After migration, verify the GIN index exists: `SELECT indexname FROM pg_indexes WHERE tablename = 'recipes' AND indexname LIKE '%search%';` — you should see at least one tsvector index.

Auth

Email/password sign-in for users who save favorites or submit their own recipes. Browsing and viewing recipes is fully public — no account required.

  1. 1In Cloud tab → 'Users & Auth', confirm Email provider is enabled.
  2. 2No additional configuration needed — the default email/password flow works out of the box.
  3. 3To promote the first author to admin (for recipe moderation): `UPDATE profiles SET role = 'admin' WHERE id = (SELECT id FROM auth.users WHERE email = 'your@email.com');`

Storage

Recipe cover photos and step-by-step photos are stored in the 'recipe-images' bucket. Images must be compressed client-side before upload to stay under the 5MB default limit and within the 1GB Free tier.

  1. 1In Cloud tab → 'Storage', create a bucket named 'recipe-images' and set it to Public.
  2. 2Add Storage policies: INSERT for authenticated users, public SELECT for all.
  3. 3Client-side compression (browser-image-compression library, 1600px max, ~300KB target) must be added to the recipe submission form — see the starter prompt and production gotchas.

Preflight checklist

  • You are in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded).
  • Lovable Free tier is enough for the full build — the prompt chain runs ~40-80 credits, fits in the monthly cap across 2-3 days. Pro $25/mo only if you iterate heavily on UI design or add the AI recipe parser.
  • Decide before pasting the starter: will you seed the app with pre-loaded recipes or let users submit their own? The starter supports both. If seeding, prepare a CSV of recipe data to insert via the SQL Editor after the schema is created.
  • Critical architectural decision — confirm you want separate ingredients and steps tables (not a text blob). This is non-negotiable if you ever want servings scaling, shopping lists, or dietary filters.

The starter prompt

Copy this. Paste it into Lovable Build mode (the default chat at the bottom-left of the editor). Hit send.

lovable-agent-mode.txt
~35-45 credits
Build a visual recipe app using Vite + React + TypeScript + Tailwind CSS + shadcn/ui, backed by Supabase (Lovable Cloud). Here is the exact schema, RLS, routes, and components to scaffold:

**CRITICAL ARCHITECTURAL REQUIREMENT:** Model ingredients and steps as SEPARATE TABLES with a `position` column for ordering — NOT as a text blob or markdown in the recipes table. This is the only way servings scaling, shopping lists, and dietary filters work later.

**Database migration (0001_recipe_schema.sql):**

Create six tables:

1. `profiles` — id uuid references auth.users pk, full_name text, avatar_path text, bio text, role text default 'user'. RLS: public-read (for author byline), per-user write own.

2. `categories` — id uuid pk, slug text unique, name text, emoji text. RLS: public-read, admin-write via is_admin().

3. `recipes` — id uuid pk default gen_random_uuid(), author_id uuid references auth.users, slug text unique not null, title text not null, summary text, cover_image_path text, prep_min int, cook_min int, servings int default 4, difficulty text check (difficulty in ('easy','medium','hard')) default 'easy', category_id uuid references categories, dietary_tags text[] default '{}', status text check (status in ('draft','published')) default 'published', search_vector tsvector generated always as (to_tsvector('english', title || ' ' || coalesce(summary, '') || ' ' || array_to_string(dietary_tags, ' '))) stored, created_at timestamptz default now(). RLS: public-read where status='published', per-author write via author_id=auth.uid(), admin-write via is_admin().

4. `ingredients` — id uuid pk, recipe_id uuid references recipes on delete cascade, position int not null, qty numeric, unit text, name text not null, notes text. RLS: public-read where parent recipe is published (EXISTS check on recipes table), per-author write, admin-write. Add unique constraint on (recipe_id, position).

5. `steps` — id uuid pk, recipe_id uuid references recipes on delete cascade, position int not null, text text not null, image_path text, time_min int. RLS: same as ingredients — public-read via parent recipe, per-author write. Add unique constraint on (recipe_id, position).

6. `favorites` — id uuid pk, user_id uuid references auth.users not null, recipe_id uuid references recipes not null, saved_at timestamptz default now(), unique (user_id, recipe_id). RLS: per-user read+write own via auth.uid()=user_id.

Create the is_admin() SECURITY DEFINER plpgsql function:
```sql
CREATE OR REPLACE FUNCTION is_admin() RETURNS boolean
LANGUAGE plpgsql SECURITY DEFINER AS $$
BEGIN
  RETURN (SELECT role FROM public.profiles WHERE id = auth.uid()) = 'admin';
END;
$$;
```

Add: GIN index on recipes.search_vector, partial index on recipes(created_at DESC) where status='published'.

**Layouts:**
- `PublicLayout.tsx` — top nav with Logo / Browse / Categories / Favorites (visible if signed in) / Sign in, plus a footer. Nav includes a search bar that routes to /search?q=.

**Pages and routes:**
- `/` — recipe grid, newest published first, 3-column desktop / 2-column tablet / 1-column mobile. Category filter chips across the top. Show dietary tag chips on each RecipeCard.
- `/recipe/[slug]` — recipe detail. Left column: cover photo + metadata (prep/cook/servings/difficulty/dietary tags). Right column: IngredientList with ServingsScaler (+ and - buttons that proportionally rescale all qty values in memory). Below: StepList with each step numbered, step image if present, and time_min timer label. Show author byline using profiles. Show FavoriteButton (heart icon) in the header — prompts sign-in if not authenticated.
- `/category/[slug]` — filtered recipe grid by category.
- `/search?q=&diet=` — full-text search results using Postgres tsvector query + dietary_tags array overlap filter.
- `/favorites` — signed-in user's saved recipes grid. Redirect to /signin if not authenticated.
- `/submit` — signed-in users add recipes. Multi-step form: (1) basics (title, category, difficulty, prep/cook/servings, dietary tags multi-select), (2) ingredients (add rows with qty, unit, name, notes + drag reorder via @dnd-kit/core), (3) steps (add rows with text + optional step image + optional time_min), (4) cover photo upload + summary + review → submit.
- `/author/[handle]` — author profile (full_name, bio, avatar) + grid of their published recipes.
- `/profile` — signed-in user edits their profile.
- `/signin` — Supabase Auth form.

**Key components:**
- `RecipeCard.tsx` — cover image (AspectRatio 4:3), title, summary snippet, difficulty badge, total time (prep+cook), dietary tag chips, FavoriteButton.
- `IngredientList.tsx` — receives ingredients array + current servings multiplier. Renders each ingredient as `qty * multiplier [unit] [name] ([notes])`. Uses formatQuantity helper that converts decimals to fractions (0.5→'1/2', 0.333→'1/3', 1.5→'1 1/2') using the fraction.js library.
- `ServingsScaler.tsx` — shows current servings count with + and - buttons. Updates a `multiplier` state in the parent (RecipeDetail). Does not write to DB — purely client-side rescaling.
- `StepList.tsx` — numbered list of steps. Each step shows position number, text, optional step image, optional 'X min' timer label. Steps are always ordered by position ASC.
- `FavoriteButton.tsx` — heart icon toggle. On click, upserts favorites (favorite_id, user_id, recipe_id) if not favorited, deletes if already favorited. Shows filled heart if favorited, outline heart if not. Optimistic update — toggle immediately, then sync with DB.
- `DietaryTagChips.tsx` — array of colored pill badges for dietary_tags values.
- `RecipeForm.tsx` — multi-step submission form with drag-reorder for ingredients and steps.

**Data fetching patterns:**
- Recipes + ingredients + steps: `supabase.from('recipes').select('*, ingredients(* ORDER BY position ASC), steps(* ORDER BY position ASC), categories(*), profiles(full_name, avatar_path, bio)').eq('slug', slug).single()`
- Favorites check: `supabase.from('favorites').select('id').eq('user_id', userId).eq('recipe_id', recipeId).maybeSingle()`
- Search: `supabase.from('recipes').select('*').textSearch('search_vector', query, { type: 'websearch' }).filter('dietary_tags', 'cs', dietFilter ? `{${dietFilter}}` : '{}')`

**Styling:** warm food-magazine feel — generous cover photos, serif headings (import 'Playfair Display' from Google Fonts for recipe titles), comfortable reading line-height; shadcn Card, AspectRatio, Badge (for dietary tags), Toggle (for FavoriteButton), Carousel (for step images).

**Deliverables:** schema migration with 6 tables + RLS + is_admin() + GIN index, public recipe grid + detail with ServingsScaler + IngredientList + StepList, category filter, full-text search with dietary filter, favorites toggle, recipe submission multi-step form with image upload, author page.

What this prompt generates

  • Generates the full migration with 6 normalized tables — ingredients and steps as separate tables with position ordering — plus RLS and the is_admin() function.
  • Scaffolds the public recipe grid, detail page with ServingsScaler and IngredientList, category filter, and full-text search with dietary tag filter.
  • Creates the FavoriteButton with optimistic update and favorites page for signed-in users.
  • Builds the multi-step recipe submission form with ingredient/step drag-reorder and image upload to Storage.
  • Wires the author page and profile edit page.
  • Adds the formatQuantity helper that renders fractions cleanly (0.333 → '1/3').

Paste into: Lovable Build mode (the default chat at the bottom-left of the editor)

Expected output

What Lovable will generate after the starter prompt runs successfully.

Files
src/layouts/PublicLayout.tsx

Top nav with search bar, categories, and favorites link

src/pages/Home.tsx

Public recipe grid with category filter chips

src/pages/RecipeDetail.tsx

Full recipe with ServingsScaler, IngredientList, StepList

src/pages/Category.tsx

Category-filtered recipe grid

src/pages/Search.tsx

Full-text search results with dietary filter

src/pages/Favorites.tsx

Signed-in user's saved recipes grid

src/pages/Submit.tsx

Multi-step recipe submission form

src/pages/Author.tsx

Author profile + their published recipes

src/pages/Profile.tsx

Signed-in user's profile editor

src/components/RecipeCard.tsx

Cover image + metadata card for grids

src/components/IngredientList.tsx

Rendered ingredient rows with fraction formatting

src/components/ServingsScaler.tsx

Plus/minus servings adjuster for client-side rescaling

src/components/StepList.tsx

Numbered step list always sorted by position ASC

src/components/FavoriteButton.tsx

Heart toggle with optimistic update

src/components/DietaryTagChips.tsx

Colored pill badges for dietary tags

src/components/RecipeForm.tsx

Multi-step submission form with drag-reorder

src/hooks/useFavorites.ts

Favorites state with upsert/delete and optimistic update

src/lib/formatQuantity.ts

Converts decimals to readable fractions using fraction.js

supabase/migrations/0001_recipe_schema.sql

6 tables + RLS + is_admin() + GIN index

Routes
/

Public recipe grid with category filter chips

/recipe/[slug]

Full recipe detail with ServingsScaler

/category/[slug]

Category-filtered recipe grid

/search

Full-text search with dietary tag filter

/favorites

Signed-in user's saved recipes

/submit

Multi-step recipe submission form

/author/[handle]

Author profile + recipes

/profile

User profile editor

/signin

Auth form

DB Tables
recipes
ColumnType
iduuid primary key
author_iduuid references auth.users
slugtext unique not null
titletext not null
summarytext
cover_image_pathtext
prep_minint
cook_minint
servingsint default 4
difficultytext check in ('easy','medium','hard')
category_iduuid references categories
dietary_tagstext[] default '{}'
statustext check in ('draft','published') default 'published'
search_vectortsvector generated always as stored

RLS: Public-read where status='published'. Per-author write via author_id=auth.uid(). Admin-write via is_admin().

ingredients
ColumnType
iduuid primary key
recipe_iduuid references recipes on delete cascade
positionint not null
qtynumeric
unittext
nametext not null
notestext

RLS: Public-read where parent recipe is published. Per-author write. Unique (recipe_id, position).

steps
ColumnType
iduuid primary key
recipe_iduuid references recipes on delete cascade
positionint not null
texttext not null
image_pathtext nullable
time_minint nullable

RLS: Public-read where parent recipe is published. Per-author write. Unique (recipe_id, position).

favorites
ColumnType
iduuid primary key
user_iduuid references auth.users not null
recipe_iduuid references recipes not null
saved_attimestamptz default now()

RLS: Per-user read+write own via auth.uid()=user_id. Unique (user_id, recipe_id).

categories
ColumnType
iduuid primary key
slugtext unique
nametext
emojitext

RLS: Public-read. Admin-write via is_admin().

profiles
ColumnType
iduuid primary key references auth.users
full_nametext
avatar_pathtext
biotext
roletext default 'user'

RLS: Public-read (for author bylines). Per-user write own.

Components
RecipeCard

Cover image + metadata card with dietary tag chips

IngredientList

Rendered ingredients with fraction-formatted quantities

ServingsScaler

Plus/minus buttons for client-side servings rescaling

StepList

Numbered steps always sorted by position ASC

FavoriteButton

Heart toggle with optimistic update and sign-in prompt for anon

DietaryTagChips

Colored pill badges for dietary_tags array

RecipeForm

Multi-step submission form with drag-reorder for ingredients and steps

Follow-up prompts

Paste these into Agent Mode one by one, in order, after the starter prompt finishes.

1

Servings scaler with fraction formatting

Client-side servings rescaling with proper fraction display (0.333 → '1/3', 1.5 → '1 1/2')

~15-20 credits
prompt
Implement the servings scaler and fraction formatter in RecipeDetail.tsx:

1. Install fraction.js: it is already available as a module — `import Fraction from 'fraction.js';`

2. Create src/lib/formatQuantity.ts:
```typescript
import Fraction from 'fraction.js';
export function formatQuantity(qty: number | null, multiplier: number): string {
  if (qty === null) return '';
  const scaled = qty * multiplier;
  const frac = new Fraction(scaled).simplify(0.01);
  const whole = Math.floor(frac.valueOf());
  const remainder = frac.sub(whole);
  if (remainder.valueOf() === 0) return String(whole || '');
  const fracStr = remainder.toFraction();
  return whole > 0 ? `${whole} ${fracStr}` : fracStr;
}
```

3. In RecipeDetail.tsx, add state: `const [servings, setServings] = useState(recipe.servings);`
The multiplier is `servings / recipe.servings`.

4. ServingsScaler.tsx:
```typescript
export function ServingsScaler({ servings, onChange }: { servings: number; onChange: (n: number) => void }) {
  return (
    <div className='flex items-center gap-3'>
      <button onClick={() => onChange(Math.max(1, servings - 1))} className='w-8 h-8 rounded-full border'>-</button>
      <span className='font-medium'>{servings} servings</span>
      <button onClick={() => onChange(servings + 1)} className='w-8 h-8 rounded-full border'>+</button>
    </div>
  );
}
```

5. In IngredientList.tsx, use formatQuantity for each ingredient's qty display:
```typescript
<span>{formatQuantity(ingredient.qty, multiplier)} {ingredient.unit} {ingredient.name}</span>
```

Verify: with 4 servings as base, set to 3 → '1 cup' should become '3/4 cup', '1/3 cup' should become '1/4 cup'.

When to use: Right after the starter finishes — the servings scaler is the most demo-able feature and sets the quality bar

2

Recipe submission with cover and step image upload

Cover photo and step-by-step image upload with client-side compression to keep files under 300KB

~35-45 credits
prompt
Upgrade the recipe submission form to support image upload with client-side compression:

1. Install browser-image-compression: it is available as a module for client-side use.

2. In RecipeForm.tsx, on every image file input (cover photo and step images), compress before upload:
```typescript
import imageCompression from 'browser-image-compression';
const compressed = await imageCompression(file, { maxSizeMB: 0.3, maxWidthOrHeight: 1600 });
const path = `recipes/${recipeId}/${type}/${filename}`;
const { error } = await supabase.storage.from('recipe-images').upload(path, compressed);
```

3. After upload, store only the path (not the full URL) in cover_image_path or steps.image_path. Compute the public URL on read:
```typescript
const { data } = supabase.storage.from('recipe-images').getPublicUrl(path);
// data.publicUrl is the full CDN URL
```

4. Add a drag-and-drop zone for the cover photo (shadcn's file input or a custom drop zone with onDrop).

5. For step images, show a small 'Add photo' button next to each step row in the step editor. Show a thumbnail preview after upload.

6. Show upload progress: `const progress = (loaded / total * 100).toFixed(0) + '%';` using the supabase.storage.upload() onUploadProgress callback.

Verify: upload a 10MB phone photo — it should compress to under 300KB before uploading.

When to use: After the servings scaler — only if you want user-generated recipes; skip if you are seeding the DB directly

3

Shopping list generator from multiple favorites

Shopping list generator that combines ingredients from multiple saved recipes with deduplication

~45-55 credits
prompt
Add a shopping list feature: signed-in users select multiple saved recipes and generate a combined ingredient list.

1. In Favorites.tsx, add a multi-select mode (checkbox on each RecipeCard). Show a 'Generate Shopping List' button when 2+ recipes are selected.

2. Create src/pages/ShoppingList.tsx that receives a list of recipe_ids (via router state or query params). Fetches all ingredients for those recipes:
```typescript
const { data } = await supabase.from('ingredients')
  .select('*, recipes(servings, title)')
  .in('recipe_id', recipeIds)
  .order('name');
```

3. Combine ingredients by (name, unit): group rows with the same name and unit, sum their qty values (scaled by the recipe's servings if needed). Use a Map:
```typescript
const combined = new Map<string, { qty: number; unit: string; name: string; sources: string[] }>();
for (const ing of ingredients) {
  const key = `${ing.name}|||${ing.unit || ''}`;
  const existing = combined.get(key);
  if (existing) {
    existing.qty += ing.qty ?? 0;
    existing.sources.push(ing.recipes.title);
  } else {
    combined.set(key, { qty: ing.qty ?? 0, unit: ing.unit || '', name: ing.name, sources: [ing.recipes.title] });
  }
}
```

4. Render the combined list grouped by category (if available) or alphabetically. Each row shows combined qty (formatted with formatQuantity) + unit + name + '(for RecipeA, RecipeB)'.

5. Add a 'Print list' button that calls window.print() on a print-optimized stylesheet.

When to use: After the core recipe detail and favorites are working — this is the 'magical' feature that makes the app sticky

4

AI recipe parser from URL

AI-powered recipe import from any URL using Claude Haiku 4.5 for structured extraction

~45-55 credits
prompt
Add an 'Import recipe from URL' button to the Submit page that fetches a recipe URL, extracts structured ingredients and steps, and pre-fills the submission form.

1. Create supabase/functions/parse-recipe-url/index.ts:
   - Receive { url } in the request body.
   - Fetch the URL with `await fetch(url)` and get the HTML text.
   - Call Claude Haiku 4.5 (the fastest Anthropic model for structured extraction):
```typescript
const anthropicRes = await fetch('https://api.anthropic.com/v1/messages', {
  method: 'POST',
  headers: { 'x-api-key': Deno.env.get('ANTHROPIC_API_KEY'), 'anthropic-version': '2024-06-01', 'content-type': 'application/json' },
  body: JSON.stringify({
    model: 'claude-haiku-4-5',
    max_tokens: 2000,
    messages: [{ role: 'user', content: `Extract recipe from this HTML and return JSON with: title, summary, prep_min, cook_min, servings, dietary_tags (array), ingredients (array of {qty, unit, name, notes}), steps (array of {text, time_min}). HTML: ${html.slice(0, 8000)}` }]
  })
});
```
   - Parse the JSON from the response content.
   - Return the structured recipe data.

2. In Submit.tsx, add an 'Import from URL' modal at the top of the form. On submit, call the edge function and pre-fill all form fields. Allow the user to review and edit before submitting.

3. Add ANTHROPIC_API_KEY to Cloud → Secrets (never as VITE_ANTHROPIC_KEY, never in /src/ files).

Verify by importing a recipe from AllRecipes.com — the form should pre-fill with title, ingredients, and steps.

When to use: Optional — high-value UX win for users who want to save recipes from external sites; requires Anthropic API key in Secrets

5

Print-friendly recipe view

Print-optimized recipe view with two-column layout and auto-print dialog

~15-20 credits
prompt
Add a print-optimized view at /recipe/[slug]/print that renders the recipe in a clean, paper-friendly layout.

1. Create src/pages/RecipePrint.tsx — renders the full recipe with:
   - Large recipe title and metadata (prep time, cook time, servings, difficulty) in a single header row.
   - Two-column layout: ingredients left, steps right (on paper).
   - All step images and the cover image excluded (use CSS @media print to hide images, or include a low-res version).
   - Font: serif font (Playfair Display or Georgia) at 12pt for print.
   - No nav, no sidebar, no footer.

2. Add a print stylesheet via a `<style>` block with `@media print { .no-print { display: none; } body { font-size: 12pt; } }`.

3. Add a 'Print recipe' button on the recipe detail page (RecipeDetail.tsx) that opens /recipe/[slug]/print in a new tab.

4. On the print page, call `window.print()` automatically after a 500ms delay so the browser print dialog opens immediately.

Verify: open the print page and preview — it should show a clean two-column layout at 12pt with no nav chrome.

When to use: Last — small effort, high signal of quality; users who print recipes are highly engaged

Common errors

Real error strings you'll see. Find yours, paste the fix prompt.

ingredients all show in wrong order

Lovable scaffolded the ingredients query without ORDER BY position — Postgres returns rows in undefined storage order without an explicit sort clause.

Fix — paste into Lovable Agent Mode
In any query that fetches ingredients or steps, always add `.order('position', { ascending: true })`. In Supabase JS nested select: `.select('*, ingredients(* ORDER BY position ASC), steps(* ORDER BY position ASC)')`. The position column exists for exactly this reason — use it everywhere.
servings scaler shows '0.333333' for 1/3 cup scaled to 3/4

The formatQuantity function is doing raw arithmetic without fraction conversion — qty * multiplier produces floating point numbers like 0.333333.

Fix — paste into Lovable Agent Mode
Install fraction.js and implement the formatQuantity helper: `import Fraction from 'fraction.js'; const frac = new Fraction(qty * multiplier).simplify(0.01);` Then format as whole + fraction string. This converts 0.333 → '1/3', 0.5 → '1/2', 1.5 → '1 1/2'. All quantity display in IngredientList.tsx must use this formatter.
image upload returns 413 Payload Too Large

Users uploaded a 10-12MB phone photo directly; Supabase Storage default upload limit is 5MB.

Fix — paste into Lovable Agent Mode
Add client-side compression in RecipeForm.tsx before every upload call: `import imageCompression from 'browser-image-compression'; const compressed = await imageCompression(file, { maxSizeMB: 0.3, maxWidthOrHeight: 1600 });` Pass `compressed` (not the original `file`) to supabase.storage.from().upload(). This reliably keeps files under 300KB.
favorite heart shows wrong state for the user

The useFavorites hook queries favorites with only one filter (user_id) but not both (user_id AND recipe_id), so it returns all favorites and the isFavorited check is incorrect.

Fix — paste into Lovable Agent Mode
In useFavorites.ts (or wherever the check happens), use both filters: `supabase.from('favorites').select('id').eq('user_id', userId).eq('recipe_id', recipeId).maybeSingle()`. The isFavorited state should be `!!data` from this query. Without the recipe_id filter, all recipes show as favorited for a user who has at least one favorite.
search returns 0 rows for valid recipe titles

The search_vector column is a GENERATED ALWAYS column that computed correctly for new rows but existing seed data has null search_vector.

Fix — paste into Lovable Agent Mode
Run in Cloud → Database → SQL Editor: `UPDATE recipes SET title = title;` This touches every row and triggers recomputation of the generated search_vector column. Or backfill explicitly: `UPDATE recipes SET search_vector = to_tsvector('english', title || ' ' || coalesce(summary, '') || ' ' || array_to_string(dietary_tags, ' '));` Run this once after seeding.
step images show as broken links in production

The image path stored in steps.image_path is a full URL from the dev Supabase project, or points to a different project ID in production.

Fix — paste into Lovable Agent Mode
Never store the full URL in image_path columns — store only the path relative to the bucket root (e.g. 'recipes/abc-uuid/steps/step-1.jpg'). Compute the public URL at render time: `const { data } = supabase.storage.from('recipe-images').getPublicUrl(step.image_path); const url = data.publicUrl;` This works correctly in both dev and production environments.

Cost reality

What this build actually costs — no surprises on your card.

Recommended Lovable plan

Free is enough for the core build — the full starter + servings scaler chain runs ~40-80 credits, which fits in Free's monthly cap across 2-3 days. Pro $25/mo only if you iterate heavily on the visual design or add the AI recipe parser in one session.

Monthly run cost breakdown

~40-80 credits: starter ~40, servings scaler ~20, image upload ~40, shopping list ~50; AI recipe parser adds ~50 more. Total with full chain: ~80-150 credits total
ItemCost
Lovable

Stop iterating after the build is complete

Free $0
Supabase

Recipe tables stay tiny for years; 10K recipes is well under 500MB

Free $0
Supabase Storage

Always compress images before upload — phone photos at full res fill 1GB in ~50 recipes

Free $0 (1GB included)
Custom domain

Optional — works on lovable.app subdomain

~$12/yr
Anthropic API (if AI parser added)

Claude Haiku 4.5; negligible cost at low volume

~$0.002/recipe import

Scaling notes: Storage 1GB Free tier breaks first if users upload uncompressed step images — always compress to 300KB max client-side. Supabase Free 500MB DB handles ~500K recipe rows with ingredients and steps. At that scale you will be on Supabase Pro $25/mo long before DB size is the issue — traffic and concurrent connections are more likely the first bottleneck.

Production checklist

Steps to take before you share the URL with real users.

Domain & SSL

  • Connect your custom domain

    Click Publish (top-right) → Settings → Custom Domain → enter domain → follow CNAME or A record DNS instructions. SSL provisioned automatically.

Image Compression

  • Verify client-side compression is active before accepting user submissions

    In the submission form, upload a 10MB test photo and check Cloud → Storage → recipe-images — the uploaded file should be under 300KB. If it is 10MB, the browser-image-compression call is not being applied before upload. Check the RecipeForm.tsx image upload handler.

Database

  • Backfill search_vector after initial recipe seeding

    After seeding the recipes table via SQL Editor, run: `UPDATE recipes SET title = title;` to trigger the generated column recomputation. Then verify: `SELECT count(*) FROM recipes WHERE search_vector IS NULL;` — should return 0.

Monitoring

  • Check Storage usage monthly

    In Cloud tab → Storage → recipe-images → check total size. At 800MB+ on the Free tier (1GB limit), either enable image compression if not already applied or upgrade to Supabase Pro $25/mo for 8GB storage.

Frequently asked questions

Why separate ingredients into their own table instead of just a text field?

A text blob makes every interesting feature impossible. With ingredients stored as plain text, you cannot scale a recipe from 4 to 6 servings (you would have to parse natural language like '1 cup' from unstructured text). You cannot generate a shopping list that combines ingredients from multiple recipes. You cannot filter by dietary content. You cannot search by ingredient name. With a normalized table (recipe_id, position, qty, unit, name), all of these become straightforward queries and client-side math. The servings scaler, shopping list, and dietary filter are the concrete payoffs.

How does the servings scaler handle fractions cleanly?

The formatQuantity helper uses the fraction.js library. When you scale from 4 servings to 3, the multiplier is 0.75. A quantity of 1 cup becomes `1 * 0.75 = 0.75`. Without formatting, this shows as '0.75 cup'. With fraction.js, new Fraction(0.75).toFraction() returns '3/4', so it displays as '3/4 cup'. The library also handles mixed numbers: 1.5 becomes '1 1/2', and 0.333... becomes '1/3'. This is why the formatQuantity helper is a non-negotiable part of the IngredientList component.

Can users submit their own recipes, and how do I prevent spam?

Yes — the /submit route allows signed-in users to post recipes. Requiring authentication is the primary spam prevention: bots cannot easily create accounts at scale the way they can submit anonymous forms. New submissions default to status='published' (visible immediately), but you can change the default to status='draft' and add an admin review step at /admin/recipes if your community is open to the public. The admin role (via is_admin()) can edit or delete any recipe.

How big can my recipe collection grow before Supabase free tier breaks?

Very large. A recipe row is roughly 1KB of text data. With 6 ingredients (each ~100 bytes) and 8 steps (each ~200 bytes), one full recipe is roughly 3KB. At that rate, 500MB of Supabase Free storage holds approximately 166,000 recipes. Your Storage limit (1GB for images) is far more likely to be the first bottleneck if users upload step-by-step photos at high resolution. Always compress to 300KB max client-side — this extends your Storage tier to approximately 3,000 cover photos within the 1GB free tier.

Should I use the Lovable AI panel to auto-generate recipes from photos?

The Lovable AI panel (Gemini 3 Flash) is not ideal for recipe extraction from photos — it can describe what it sees in an image but cannot reliably output structured ingredient lists with quantities and units. For photo-based recipe extraction, a dedicated vision model (GPT-5.4 or Claude Sonnet 4.6 with vision) works much better. The AI recipe parser follow-up prompt uses Claude Haiku 4.5 to extract structured data from recipe URLs (not photos), which is a more reliable use case for structured extraction.

Can I add a meal-planning calendar later without rebuilding the schema?

Yes. The schema is designed to be extensible. A meal plan calendar adds one new table: `meal_plans` with (user_id, recipe_id, planned_date, meal_type [breakfast/lunch/dinner/snack]). This table joins to the existing recipes table — no schema changes to recipes, ingredients, or steps. The shopping list generator (follow-up prompt) can then be extended to accept a date range and pull all meal-planned recipes for that week.

How does this compare to just using WordPress with a recipe plugin?

A WordPress recipe plugin (Tasty Recipes, WP Recipe Maker) gives you schema.org structured data, recipe card SEO markup, and print layouts out of the box — things you have to build manually in Lovable. WordPress also has SSR so recipe pages rank on Google immediately. The Lovable recipe app wins on: full data ownership with a clean Postgres schema, no plugin conflicts, ability to add custom features (shopping lists, community features, dietary filters) without a $100+/yr premium plugin, and a modern React interface. Build in Lovable when you want owned infrastructure and plan to extend the app; use WordPress when your primary goal is Google search traffic for recipe pages.

If your recipe app needs nutrition database integration, meal kit delivery, or a mobile app, what does RapidDev offer?

If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.

Need a production-grade version?

RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.