Build a shareable recipe app with V0 using Next.js and Supabase that lets users store, discover, and share recipes with adjustable serving sizes and ingredient scaling. Features a recipe feed with tag filters, image galleries, and a favorites system — all in about 30-60 minutes.
What you're building
Home cooks and food bloggers need a clean, fast app to organize recipes, adjust serving sizes on the fly, and share dishes with friends. Existing recipe sites are bloated with ads and life stories — your app gets straight to the ingredients and steps.
V0 makes this a quick build by generating the recipe card grid, detail page with scaling logic, and creation form from a few prompts. Supabase handles the database and image storage. Server Actions handle all the form submissions without needing API routes.
The architecture uses Next.js App Router with Server Components for the recipe feed (great for SEO), a client component for the interactive ingredient scaler, Server Actions for creating recipes and toggling favorites, and Supabase Storage for recipe images.
Final result
A recipe collection app with a searchable feed, recipe detail pages with adjustable serving sizes, a structured creation form, and a favorites system — all with clean, fast-loading pages.
Tech stack
Prerequisites
- A V0 account (free tier works for this project)
- A Supabase project (free tier works — connect via V0's Connect panel)
- A few recipes to add for testing
Build steps
Set up the project and recipe schema
Open V0 and create a new project. Use the Connect panel to add Supabase. Prompt V0 to create the schema for recipes, ingredients, instructions, tags, and favorites.
1// Paste this prompt into V0's AI chat:2// Build a recipe app. Create a Supabase schema with:3// 1. recipes: id (uuid PK), author_id (uuid FK), title (text), description (text), image_url (text), prep_time_minutes (integer), cook_time_minutes (integer), servings (integer), difficulty (text check easy/medium/hard), cuisine (text), is_published (boolean default false), created_at (timestamptz)4// 2. ingredients: id (uuid PK), recipe_id (uuid FK), name (text), amount (numeric), unit (text), position (integer)5// 3. instructions: id (uuid PK), recipe_id (uuid FK), step_number (integer), description (text), image_url (text nullable)6// 4. tags: id (uuid PK), name (text unique)7// 5. recipe_tags: recipe_id (uuid FK), tag_id (uuid FK), primary key (recipe_id, tag_id)8// 6. favorites: id (uuid PK), user_id (uuid FK), recipe_id (uuid FK), created_at (timestamptz) with unique constraint on (user_id, recipe_id)9// RLS: anyone can SELECT published recipes, only authors can INSERT/UPDATE their own.10// Generate SQL migration and TypeScript types.Pro tip: Use V0's Design Mode (Option+D) for tweaking the recipe card grid — adjust image heights, spacing, and typography completely free.
Expected result: Supabase is connected with all six tables created. RLS policies allow public reading of published recipes while protecting author-only editing.
Build the recipe feed with search and tag filters
Create the home page showing a grid of recipe cards with search and tag-based filtering. Each card displays the recipe image, title, prep/cook time, difficulty badge, and cuisine tag.
1// Paste this prompt into V0's AI chat:2// Build a recipe feed at app/page.tsx.3// Requirements:4// - Top section: Input for search by recipe title, ToggleGroup for cuisine filters (All, Italian, Mexican, Asian, American, Indian, Other)5// - Grid of recipe Cards (3 columns desktop, 2 tablet, 1 mobile)6// - Each Card shows:7// - Recipe image (Skeleton while loading)8// - Title text9// - Prep + cook time as "45 min total"10// - difficulty Badge (easy=green, medium=yellow, hard=red)11// - Cuisine tag as Badge12// - Avatar for recipe author13// - Cards link to /recipes/[id]14// - Search filters update URL searchParams for shareable URLs15// - Server Components for data fetching from Supabase16// - Fetch published recipes ordered by created_at descendingExpected result: A recipe feed with search input, cuisine filter toggles, and a responsive grid of recipe Cards with images, timing info, and difficulty Badges.
Create the recipe detail page with ingredient scaling
Build the recipe detail page with the key feature — a serving size Slider that dynamically recalculates ingredient amounts. The scaler uses a client component since it needs interactivity.
1'use client'23import { useState } from 'react'4import { Slider } from '@/components/ui/slider'5import { Card } from '@/components/ui/card'6import { Separator } from '@/components/ui/separator'78interface Ingredient {9 name: string10 amount: number11 unit: string12}1314export function IngredientScaler({15 ingredients,16 baseServings,17}: {18 ingredients: Ingredient[]19 baseServings: number20}) {21 const [servings, setServings] = useState(baseServings)22 const ratio = servings / baseServings2324 return (25 <Card className="p-6">26 <div className="flex items-center justify-between mb-4">27 <h3 className="text-lg font-semibold">Ingredients</h3>28 <span className="text-sm text-muted-foreground">29 {servings} servings30 </span>31 </div>32 <Slider33 value={[servings]}34 onValueChange={([v]) => setServings(v)}35 min={1}36 max={baseServings * 4}37 step={1}38 className="mb-6"39 />40 <ul className="space-y-2">41 {ingredients.map((ing, i) => {42 const scaled = Math.round(ing.amount * ratio * 100) / 10043 return (44 <li key={i} className="flex justify-between text-sm">45 <span>{ing.name}</span>46 <span className="font-medium">47 {scaled} {ing.unit}48 </span>49 </li>50 )51 })}52 </ul>53 <Separator className="mt-4" />54 </Card>55 )56}Expected result: An ingredient list with a Slider that adjusts serving size. Moving the Slider recalculates all ingredient amounts proportionally in real time.
Build the recipe creation form
Create a form where users add recipes with structured ingredients, step-by-step instructions, and image upload. Each ingredient has name, amount, and unit fields. Instructions are numbered steps.
1// Paste this prompt into V0's AI chat:2// Build a recipe creation form at app/recipes/new/page.tsx.3// Requirements:4// - Protected by auth5// - Form sections in shadcn/ui Cards:6// - Basic Info: Input for title, Textarea for description, Select for difficulty (easy/medium/hard), Select for cuisine, Input for prep_time and cook_time, Input for servings7// - Image: file upload zone with preview, uploads to Supabase Storage 'recipe-images' bucket8// - Ingredients: dynamic list — each row has Input for name, Input type=number for amount, Select for unit (cup/tbsp/tsp/oz/g/lb/ml/whole). Add/Remove Buttons.9// - Instructions: dynamic numbered list — each step has Textarea for description. Add/Remove Buttons with reorder.10// - Tags: ToggleGroup for common tags (breakfast/lunch/dinner/dessert/snack/vegetarian/vegan/gluten-free)11// - Submit via Server Action createRecipe() that inserts recipe, ingredients, instructions, and tags12// - After creation, redirect to the new recipe page13// - 'use client' for the interactive formPro tip: Queue this prompt right after the schema prompt using V0's prompt queuing. The form will be ready by the time you finish reviewing the schema.
Expected result: A multi-section recipe creation form with dynamic ingredient rows, numbered instruction steps, image upload, and tag selection. Submitting creates the full recipe.
Complete code
1'use server'23import { createClient } from '@/lib/supabase/server'4import { revalidatePath } from 'next/cache'5import { redirect } from 'next/navigation'67export async function createRecipe(formData: FormData) {8 const supabase = await createClient()9 const { data: { user } } = await supabase.auth.getUser()1011 const { data: recipe } = await supabase12 .from('recipes')13 .insert({14 author_id: user?.id,15 title: formData.get('title') as string,16 description: formData.get('description') as string,17 prep_time_minutes: parseInt(formData.get('prep_time') as string),18 cook_time_minutes: parseInt(formData.get('cook_time') as string),19 servings: parseInt(formData.get('servings') as string),20 difficulty: formData.get('difficulty') as string,21 cuisine: formData.get('cuisine') as string,22 image_url: formData.get('image_url') as string,23 is_published: true,24 })25 .select()26 .single()2728 if (!recipe) throw new Error('Failed to create recipe')2930 const ingredients = JSON.parse(formData.get('ingredients') as string)31 await supabase.from('ingredients').insert(32 ingredients.map((ing: any, i: number) => ({33 recipe_id: recipe.id,34 name: ing.name,35 amount: ing.amount,36 unit: ing.unit,37 position: i + 1,38 }))39 )4041 const steps = JSON.parse(formData.get('instructions') as string)42 await supabase.from('instructions').insert(43 steps.map((step: string, i: number) => ({44 recipe_id: recipe.id,45 step_number: i + 1,46 description: step,47 }))48 )4950 revalidatePath('/')51 redirect(`/recipes/${recipe.id}`)52}5354export async function toggleFavorite(recipeId: string) {55 const supabase = await createClient()56 const { data: { user } } = await supabase.auth.getUser()57 if (!user) return5859 const { data: existing } = await supabase60 .from('favorites')61 .select('id')62 .eq('user_id', user.id)63 .eq('recipe_id', recipeId)64 .single()6566 if (existing) {67 await supabase.from('favorites').delete().eq('id', existing.id)68 } else {69 await supabase.from('favorites').insert({70 user_id: user.id,71 recipe_id: recipeId,72 })73 }7475 revalidatePath(`/recipes/${recipeId}`)76 revalidatePath('/favorites')77}Customization ideas
Add nutritional information
Integrate a nutrition API to automatically calculate calories, protein, carbs, and fat based on the ingredient list and display a nutritional breakdown card.
Build a meal planner
Add a weekly meal planning page where users drag recipes into day slots and auto-generate a consolidated shopping list from all planned recipes.
Add recipe sharing and social features
Generate shareable links with Open Graph images, add comments/reviews on recipes, and build a follow system for favorite recipe authors.
Add cooking mode
Create a step-by-step cooking mode with large text, voice-friendly display, and auto-advancing timer for each step.
Common pitfalls
Pitfall: Putting the ingredient Slider in a Server Component
How to avoid: Mark the ingredient scaler component with 'use client'. Pass the base ingredients and servings as props from the Server Component parent.
Pitfall: Using floating-point math for ingredient scaling without rounding
How to avoid: Round scaled amounts to 2 decimal places: Math.round(amount * ratio * 100) / 100. For common fractions, consider a fraction display library.
Pitfall: Not setting Supabase Storage bucket to public for recipe images
How to avoid: Create a public bucket named recipe-images in Supabase Storage Dashboard. Store the public URL directly in the image_url field.
Best practices
- Use Server Components for the recipe feed and detail pages to optimize SEO and page load speed
- Use V0's Design Mode (Option+D) to visually adjust recipe Card image heights and grid spacing for free
- Store recipe images in a Supabase Storage public bucket for permanent, shareable URLs
- Use Server Actions for all mutations (creating recipes, toggling favorites) — no API routes needed
- Keep the ingredient Slider in a 'use client' component and calculate scaling client-side for instant response
- Use URL searchParams for search and filter state so recipe searches are shareable and bookmarkable
AI prompts to try
Copy these prompts to build this project faster.
I'm building a recipe app with Next.js App Router and Supabase. I need a recipe creation form that handles dynamic ingredient lists and ordered instruction steps. Each ingredient has name (text), amount (number), and unit (select). Instructions are numbered text steps. Write the 'use client' form component with add/remove functionality for both sections, and the Server Action that inserts the recipe with all related ingredients and instructions in one transaction.
Create an ingredient scaling component. Accept an array of ingredients (name, amount, unit) and base servings number. Use a shadcn/ui Slider to let users adjust servings from 1 to 4x the base. Display each ingredient with the scaled amount, calculated as ingredient.amount * (selectedServings / baseServings), rounded to 2 decimals. Mark as 'use client'.
Frequently asked questions
Can I build this recipe app on V0's free tier?
Yes. The free tier gives you enough credits to generate the recipe feed, detail page, and creation form. Supabase free tier handles the database and image storage.
How does the ingredient scaling work?
When you move the Slider to change serving size, a client-side component multiplies each ingredient amount by the ratio (desiredServings / baseServings). The calculation happens instantly in the browser with no server round-trip.
Where are recipe images stored?
Recipe images upload to a Supabase Storage public bucket. The public URL is stored in the recipes.image_url field. Public buckets serve images directly without expiring signed URLs.
Can users share recipes without signing up?
Anyone can browse and view published recipes without an account. Creating recipes and saving favorites requires authentication. Connect Supabase Auth via the Connect panel for sign-up/sign-in.
How do I deploy the recipe app?
Click Share then Publish to Production in V0. Supabase credentials are auto-configured from the Connect panel. Your recipe pages are server-rendered for fast loading and good SEO.
Can RapidDev help build a custom recipe or food platform?
Yes. RapidDev has built 600+ apps including food platforms with meal planning, nutritional analysis, and subscription meal kits. Book a free consultation to discuss your idea.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation