Skip to main content
RapidDev - Software Development Agency
Lovable PromptsMarketplaceAdvanced

Build a Two-Sided Marketplace in Lovable

A two-sided marketplace where sellers list items with images, buyers browse and checkout via Stripe Connect, and platform commissions are split automatically — with multi-role RLS, seller onboarding, and admin moderation.

Time to MVP

days

Credits

~250-500+ credits for full build

Difficulty

Advanced

Cloud features

4

TL;DR

The one-paragraph version before you dive in.

Paste the starter prompt into Lovable Build mode and get a browsable marketplace where sellers list items with images and buyers checkout via Stripe. The raw-body webhook follow-up is non-negotiable before real money flows. Full build runs ~250-500 credits over several days — the hardest category in this 31-page series.

Setup checklist

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

Cloud tab settings

Database

Stores listings, profiles (with role), orders, payouts, and listing images.

  1. 1Click the + button next to Preview in the top toolbar to open the Cloud tab
  2. 2Click Database — you'll see an empty Table Editor
  3. 3Leave it empty — the starter prompt creates the migration with all 5 tables

Auth

Buyers and sellers both need accounts. Role (buyer/seller/admin) lives in JWT app_metadata so it can't be tampered with from the client.

  1. 1Cloud tab → Users & Auth
  2. 2Confirm Email sign-in is enabled (default)
  3. 3To enable Google OAuth: Social providers → Google → paste client ID + secret from Google Cloud Console
  4. 4Set Site URL to your production custom domain before going live — OAuth callbacks fail if Site URL is still the preview domain

Storage

Holds seller-uploaded listing images. Public bucket so browsing works without auth.

  1. 1Cloud tab → Storage
  2. 2Click Create bucket, name it 'listing-images', toggle Public to ON
  3. 3Note the bucket name — the starter prompt's Storage policy references it

Edge Functions

Three functions: Stripe Connect onboarding link, Stripe Checkout session creator, and the webhook receiver (must use raw body).

  1. 1Cloud tab → Edge Functions (no manual setup — starter prompt generates all three files)
  2. 2After the starter prompt runs, go to Edge Functions panel and deploy all three functions
  3. 3The webhook URL (https://your-lovable-cloud-url/functions/v1/stripe-webhook) goes into Stripe Dashboard → Developers → Webhooks → Add endpoint

Secrets (Cloud tab → Secrets)

STRIPE_SECRET_KEY

Purpose: Used by the checkout-session and connect-onboarding Edge Functions to call Stripe's API.

Where to get it: stripe.com → Dashboard → Developers → API keys → Secret key (starts with sk_test_ for test mode, sk_live_ for production).

STRIPE_WEBHOOK_SECRET

Purpose: Used by stripe-webhook Edge Function in constructEventAsync to verify the request is genuinely from Stripe.

Where to get it: stripe.com → Dashboard → Developers → Webhooks → click your endpoint → Signing secret (starts with whsec_). Only available after you've registered the webhook endpoint URL.

STRIPE_CONNECT_CLIENT_ID

Purpose: Used by the connect-onboarding-link Edge Function to generate seller KYC links for Stripe Express accounts.

Where to get it: stripe.com → Dashboard → Settings → Connect → Platforms → your platform's client_id (starts with ca_).

Preflight checklist

  • You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded)
  • You're on Pro $25/mo and have budgeted 2-3 top-ups ($15 each) — this category burns 250-500+ credits
  • You have a Stripe account with Connect enabled (Dashboard → Settings → Connect → Get started)
  • Decide your commission model BEFORE prompting: direct charge (seller charges buyer, platform takes fee) or destination charge (platform charges buyer, pays out to seller). This changes the Checkout session structure. Direct charge is simpler to start with.
  • Accept that this is a multi-day build — the webhook + RLS + image + Connect pieces each need their own iteration cycle

The starter prompt — paste this first

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

lovable-agent-mode.txt
~80-120 credits
Build a two-sided marketplace. Stack assumptions: React + Vite + TypeScript + Tailwind + shadcn/ui (already scaffolded), React Router v6, Supabase JS client against Lovable Cloud, Embla Carousel for image carousels.

## Database schema (create one migration)
Create five tables in the public schema:

1. `profiles` — id (uuid pk references auth.users(id) on delete cascade), full_name (text), role (text default 'buyer' check in ('buyer','seller','admin')), stripe_account_id (text), payouts_enabled (bool default false), created_at (timestamptz default now()). This is a display mirror only — authorization role lives in JWT app_metadata. RLS: own row read/write; a public view `profiles_public(id, full_name)` exposes only full_name for display purposes.

2. `listings` — id (uuid pk default gen_random_uuid()), seller_id (uuid not null references auth.users(id)), title (text not null), description (text), price_cents (int not null check(price_cents > 0)), category (text), status (text default 'draft' check in ('draft','active','sold','removed')), is_deleted (bool default false), created_at (timestamptz default now()). RLS: public SELECT WHERE status='active' AND is_deleted=false; seller INSERT/UPDATE/DELETE WHERE seller_id = auth.uid(); admin all via JWT app_metadata.role='admin'.

3. `orders` — id (uuid pk default gen_random_uuid()), buyer_id (uuid references auth.users(id)), listing_id (uuid references listings(id)), amount_cents (int not null), platform_fee_cents (int not null), seller_amount_cents (int not null), status (text default 'pending' check in ('pending','paid','shipped','delivered','refunded')), stripe_payment_intent_id (text unique), placed_at (timestamptz default now()). RLS: buyer reads own; seller reads via SECURITY DEFINER function can_see_order(p_order_id uuid); admin reads all.

4. `payouts` — id (uuid pk default gen_random_uuid()), seller_id (uuid references auth.users(id)), stripe_transfer_id (text unique), amount_cents (int not null), status (text check in ('pending','paid','failed')), paid_at (timestamptz). RLS: seller reads own; admin all.

5. `listing_images` — id (uuid pk default gen_random_uuid()), listing_id (uuid references listings(id)), storage_path (text not null), position (int default 0). RLS: public SELECT WHERE parent listing is active AND is_deleted=false; seller INSERT/DELETE on own listings' images.

Create a SECURITY DEFINER plpgsql function `can_see_order(p_order_id uuid)` that returns true when auth.uid() = buyer_id OR auth.uid() = (SELECT seller_id FROM listings WHERE id = (SELECT listing_id FROM orders WHERE id = p_order_id)). Use LANGUAGE plpgsql only.

## Layouts
Create four layout components:
- `PublicLayout.tsx` — sticky header with logo, search bar placeholder, category nav, Sign in / Sign up buttons. Footer with links. Used for /, /search, /listings/:id.
- `SellerLayout.tsx` — sidebar with nav (Dashboard, My Listings, Orders, Payouts). Used for /seller/* routes. AuthGuard checks JWT session.
- `BuyerLayout.tsx` — same header as PublicLayout plus user menu dropdown with My Orders, Account. Used for /buyer/* routes.
- `AdminLayout.tsx` — sidebar with Moderation Queue. Checks JWT app_metadata.role='admin'.

## Pages
- / (Home) — hero section + 'Featured Listings' grid (8 listings WHERE is_featured=true ordered by created_at DESC) + 'Recent Listings' grid (12 most-recent active listings)
- /search — listings grid with sidebar filters: category checkboxes, price range inputs (min/max), sort dropdown (newest/price-asc/price-desc). Search by title/description via ilike. Paginated 12/page.
- /listings/:id — listing detail with Embla image carousel (listing_images ordered by position), title, price, seller chip, description, 'Buy Now' button (calls create-checkout-session Edge Function). If visitor is the seller, show 'Edit listing' and 'Mark as sold' buttons instead.
- /sell — redirect to /seller/listings if payouts_enabled=true; else show ConnectOnboardingButton that calls connect-onboarding-link Edge Function.
- /seller/dashboard — total earnings (SUM of seller_amount_cents on paid orders), orders this month count, active listings count.
- /seller/listings — CRUD table. New listing form: title, description, price, category, image upload (multiple, up to 5 images to Storage bucket 'listing-images', write paths to listing_images). Status toggle draft→active only if profiles.payouts_enabled=true.
- /seller/orders — orders for own listings (via can_see_order RLS), status filter.
- /seller/payouts — payout history table.
- /buyer/orders — buyer's own orders, status badges.
- /admin/moderation — listings WHERE is_deleted=false ordered by reports count (add a simple flags count later). Remove button sets is_deleted=true.
- /checkout/success — reads session_id from query param, shows order confirmation.
- /checkout/cancel — 'Payment was cancelled' with back link.

## Edge Functions (create stubs — I will add secrets and deploy after)
1. supabase/functions/stripe-checkout-session/index.ts — accepts { listing_id }, creates Stripe Checkout session with Direct Charge for amount_cents, application_fee_amount=10% of price_cents, on_behalf_of=seller.stripe_account_id, success_url and cancel_url. Returns { url }.
2. supabase/functions/connect-onboarding-link/index.ts — creates Stripe Express account if profiles.stripe_account_id is null, then creates Account Link for onboarding. Returns { url }. After return, seller lands on /seller/dashboard.
3. supabase/functions/stripe-webhook/index.ts — STUB ONLY. Leave body as TODO comment: 'Must use req.text() for raw body + constructEventAsync. See follow-up prompt.'

## Key components
- `ListingCard.tsx` — image (aspect-ratio 4/3 object-cover), title, price, seller name, category badge
- `ImageCarousel.tsx` — Embla Carousel with dot indicators, falls back to placeholder if no images
- `SearchBar.tsx` — debounced 300ms input, syncs to URL ?q= query param
- `ConnectOnboardingButton.tsx` — calls connect-onboarding-link function, redirects seller to Stripe KYC

## Styling
Light mode default. Primary color: violet-600. ListingCard hover scale-[1.02] transition. PublicLayout header backdrop-blur on scroll. Mobile-first (most marketplace traffic is mobile — cards stack 1 col on mobile, 2 on sm, 3 on md, 4 on lg).

Generate migration first, then layouts, then pages in order above.

What this prompt generates

  • Creates a SQL migration with 5 tables, multi-role RLS policies, and the can_see_order SECURITY DEFINER function
  • Scaffolds four layout components (Public, Seller, Buyer, Admin) plus AuthGuard and admin role check
  • Generates 12 page components including the full seller listing CRUD with image uploads to Storage
  • Creates stubs for all three Stripe Edge Functions (checkout session, Connect onboarding, webhook stub)
  • Produces a browsable public listing flow from / → /search → /listings/:id that works without auth

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
supabase/migrations/0001_marketplace_schema.sql

5 tables + multi-role RLS + can_see_order SECURITY DEFINER function

src/layouts/PublicLayout.tsx

Sticky header, search, category nav, footer

src/layouts/SellerLayout.tsx

Sidebar nav for /seller/* with AuthGuard

src/layouts/BuyerLayout.tsx

Header + user menu for /buyer/* with AuthGuard

src/components/ListingCard.tsx

4:3 image card with title, price, category badge

src/components/ImageCarousel.tsx

Embla Carousel with dot indicators and placeholder fallback

src/components/SearchBar.tsx

Debounced search synced to URL query param

src/components/ConnectOnboardingButton.tsx

Calls connect-onboarding-link and redirects seller to Stripe KYC

src/pages/Home.tsx

Featured + recent listings grids

src/pages/Search.tsx

Filtered listings with sidebar and pagination

src/pages/ListingDetail.tsx

Image carousel, price, buy button, seller detail

src/pages/SellerListings.tsx

Seller CRUD with image upload and status toggle

src/pages/SellerDashboard.tsx

Earnings and orders summary

src/pages/CheckoutSuccess.tsx

Order confirmation page

supabase/functions/stripe-checkout-session/index.ts

Creates Stripe Checkout session with Connect Direct Charge

supabase/functions/connect-onboarding-link/index.ts

Creates Stripe Express account and Account Link for KYC

supabase/functions/stripe-webhook/index.ts

Stub — filled in by follow-up #1

Routes
/

Home with featured and recent listings grids

/search

Search with category filter, price range, sort, pagination

/listings/:id

Listing detail with image carousel and buy button

/sell

Seller onboarding kickoff via Connect

/seller/dashboard

Seller earnings, orders this month, active listing count

/seller/listings

Listing CRUD with image upload and status toggle

/seller/orders

Orders for own listings

/seller/payouts

Payout history

/buyer/orders

Buyer's own purchase history

/admin/moderation

Flagged listings with remove action

/checkout/success

Post-payment confirmation

/checkout/cancel

Cancelled payment fallback

DB Tables
listings
ColumnType
iduuid primary key default gen_random_uuid()
seller_iduuid not null references auth.users(id)
titletext not null
descriptiontext
price_centsint not null check(price_cents > 0)
categorytext
statustext default 'draft'
is_deletedbool default false
created_attimestamptz default now()

RLS: Public SELECT WHERE status='active' AND is_deleted=false; seller INSERT/UPDATE/DELETE WHERE seller_id=auth.uid(); admin all via JWT app_metadata

profiles
ColumnType
iduuid primary key references auth.users(id)
full_nametext
roletext default 'buyer'
stripe_account_idtext
payouts_enabledbool default false
created_attimestamptz default now()

RLS: Own row read/write; public view profiles_public exposes only (id, full_name)

orders
ColumnType
iduuid primary key default gen_random_uuid()
buyer_iduuid references auth.users(id)
listing_iduuid references listings(id)
amount_centsint not null
platform_fee_centsint not null
seller_amount_centsint not null
statustext default 'pending'
stripe_payment_intent_idtext unique
placed_attimestamptz default now()

RLS: Buyer reads own; seller reads via can_see_order() SECURITY DEFINER function; admin reads all

payouts
ColumnType
iduuid primary key default gen_random_uuid()
seller_iduuid references auth.users(id)
stripe_transfer_idtext unique
amount_centsint not null
statustext check in ('pending','paid','failed')
paid_attimestamptz

RLS: Seller reads own; admin reads all

listing_images
ColumnType
iduuid primary key default gen_random_uuid()
listing_iduuid references listings(id)
storage_pathtext not null
positionint default 0

RLS: Public SELECT when parent listing is active; seller INSERT/DELETE on own listings' images

Components
ListingCard

4:3 image, title, price, category badge — used in home and search grids

ImageCarousel

Embla Carousel with dot indicators for listing detail page

SearchBar

Debounced search synced to URL ?q= param

ConnectOnboardingButton

Initiates Stripe Express KYC for seller onboarding

CategoryFilter

Checkbox sidebar filter for /search

Follow-up prompts

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

1

Add Stripe webhook with raw body + constructEventAsync (NON-NEGOTIABLE)

Working Stripe webhook that writes orders and payouts on payment success

~60 credits
prompt
Replace the stripe-webhook Edge Function stub with a working implementation. This is the most important follow-up — do not process real money without it.

In supabase/functions/stripe-webhook/index.ts:

1. Read the raw body FIRST, before any other processing:
   const body = await req.text();
   const signature = req.headers.get('Stripe-Signature') ?? '';

2. Verify using the ASYNC variant (Deno requires this):
   const event = await stripe.webhooks.constructEventAsync(
     body,
     signature,
     Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? ''
   );
   Do NOT call req.json() — that mutates the body stream and breaks verification.

3. Handle checkout.session.completed:
   - Read event.data.object.metadata.listing_id and metadata.buyer_id
   - INSERT into orders: { buyer_id, listing_id, amount_cents, platform_fee_cents, seller_amount_cents, status: 'paid', stripe_payment_intent_id }
   - UPDATE listings SET status='sold' WHERE id=listing_id
   - INSERT into payouts: { seller_id, amount_cents: seller_amount_cents, status: 'pending' }
   - Send a Resend confirmation email to buyer if RESEND_API_KEY is set

4. Return 200 for all recognized events; return 400 only on signature failure.

Register the deployed Edge Function URL in Stripe Dashboard → Developers → Webhooks → Add endpoint. Listen for: checkout.session.completed.

When to use: IMMEDIATELY after the starter prompt, before any real money flows through the marketplace

2

Tighten RLS for public listings and image privacy

Verified public-read listing access and private order/payout isolation

~30 credits
prompt
Audit and tighten every RLS policy before going public.

1. Sign out completely and open an incognito browser window. Go to /search — you should see only active listings (status='active' AND is_deleted=false). If you see draft or removed listings, the policy is broken.

2. Paste in Build mode: 'List every RLS policy on listings, listing_images, orders, payouts, and profiles. For each policy, show the USING clause and WITH CHECK clause. Confirm: (a) listings SELECT allows anon role with USING status=''active'' AND is_deleted=false — no auth.uid() check, (b) listing_images SELECT allows anon only for images whose parent listing is active, (c) orders SELECT never allows anon, (d) payouts SELECT never allows anon.'

3. Confirm the Storage bucket 'listing-images' has a policy allowing public read. In Cloud tab → Database → SQL Editor: SELECT * FROM storage.policies WHERE bucket_id='listing-images'; — there should be at least one SELECT policy for the anon role.

4. If any anon-accessible policy requires auth.uid() IS NOT NULL, drop it and recreate without that check.

When to use: Before announcing to sellers — test in incognito as anon, buyer, seller A, and seller B

3

Add full-text search across listings

Postgres full-text search with ranking across title and description

~50 credits
prompt
Replace the current ilike search with Postgres full-text search for better results on the /search page.

1. Add a generated tsvector column to listings: ALTER TABLE listings ADD COLUMN search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', coalesce(title,'') || ' ' || coalesce(description,''))) STORED;

2. Add a GIN index: CREATE INDEX idx_listings_search ON listings USING GIN(search_vector);

3. Create an RPC function search_listings(query text, category text, min_price int, max_price int, page_size int, page_offset int) that returns a table of listings with rank score:
   SELECT *, ts_rank(search_vector, websearch_to_tsquery('english', query)) as rank
   FROM listings
   WHERE status='active' AND is_deleted=false
   AND (query IS NULL OR search_vector @@ websearch_to_tsquery('english', query))
   AND (category IS NULL OR category = $2)
   AND (min_price IS NULL OR price_cents >= min_price)
   AND (max_price IS NULL OR price_cents <= max_price)
   ORDER BY rank DESC, created_at DESC
   LIMIT page_size OFFSET page_offset;

4. Update the /search page to call supabase.rpc('search_listings', {...params}) instead of the current .from('listings').ilike() call. Pass SearchBar's debounced query value.

When to use: Once you have 100+ listings and keyword search relevance matters

4

Add buyer reviews and ratings after purchase

Post-purchase review system with star ratings on listing cards and detail pages

~70 credits
prompt
Add a post-purchase review system so buyers can rate listings after delivery.

1. Create a reviews table: id (uuid pk), order_id (uuid unique references orders(id)), listing_id (uuid references listings(id)), reviewer_id (uuid references auth.users(id)), rating (int check(rating between 1 and 5)), body (text), created_at (timestamptz). RLS: INSERT only for authenticated users WHERE reviewer_id=auth.uid() AND EXISTS(SELECT 1 FROM orders WHERE id=order_id AND buyer_id=auth.uid() AND status='delivered'); SELECT public-read.

2. Add two computed columns to listings: avg_rating (numeric) and review_count (int) — either as materialized view or computed via a Postgres VIEW reviews_summary(listing_id, avg_rating, review_count).

3. On ListingCard, show a star rating badge (shadcn Badge) with avg_rating and review_count if review_count > 0.

4. On /listings/:id detail page, below the purchase section, show a Reviews section listing all reviews with star rating, reviewer name (from profiles_public view), body text, and relative date. At the bottom, if the buyer has a delivered order for this listing and hasn't reviewed yet, show a 'Leave a review' form with 5-star picker and textarea.

When to use: When first orders are marked 'delivered' and you want social proof on listings

5

Add admin moderation queue with flag button

Buyer-facing flag button and admin moderation queue with remove and dismiss actions

~80 credits
prompt
Add listing flagging so buyers can report inappropriate content, and an admin UI to action the reports.

1. Create a flags table: id (uuid pk), listing_id (uuid references listings(id)), reporter_id (uuid references auth.users(id)), reason (text), created_at (timestamptz). RLS: INSERT for authenticated users, SELECT for admin only. UNIQUE(listing_id, reporter_id) to prevent duplicate reports from the same buyer.

2. On /listings/:id, add a 'Report listing' button (visible to signed-in buyers who are not the seller). Clicking opens a Dialog with a reason dropdown (Prohibited item / Counterfeit / Spam / Misleading description) and a Submit button. Insert into flags.

3. Create /admin/moderation page: SELECT listings with JOIN to flags COUNT, ordered by flag count DESC. For each flagged listing, show: title, seller name, flag count, flag reasons list, listing preview image, and two action buttons: 'Remove listing' (sets is_deleted=true, sends Resend email to seller) and 'Dismiss flags' (deletes all flags for that listing).

4. The Remove action should call the admin Edge Function or SQL directly via the service-role key — add it in a new Edge Function admin-remove-listing that verifies JWT app_metadata.role='admin' before executing.

When to use: Before going public — you need moderation before real users submit listings

6

Refresh Connect status on seller dashboard load

Auto-refresh of Stripe Connect KYC status on seller dashboard so payouts_enabled updates without manual intervention

~35 credits
prompt
Fix the case where a seller completes Stripe Connect KYC but still sees payouts_enabled=false on their dashboard.

Stripe does not push a redirect parameter telling your app that KYC is complete — you must poll the Account status on each dashboard load.

1. Create an Edge Function refresh-connect-status: accept GET with no body, read the caller's JWT, get profiles.stripe_account_id for that user, call stripe.accounts.retrieve(stripe_account_id), check account.payouts_enabled and account.charges_enabled, UPDATE profiles SET payouts_enabled=true WHERE id=caller_uid if both are true, return { payouts_enabled, charges_enabled }.

2. On SellerDashboard mount (useEffect), if profiles.stripe_account_id is not null AND payouts_enabled is false, call supabase.functions.invoke('refresh-connect-status'). If the response shows payouts_enabled=true, refetch the profiles row.

3. Show a status banner at the top of SellerDashboard: 'Verifying your payout account...' while the function is in flight; 'Payouts active' with a green check when payouts_enabled=true; 'Complete your payout setup' with a ConnectOnboardingButton if still false after checking.

When to use: Immediately after a seller completes the Stripe KYC flow and returns to your app

Common errors

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

SubtleCryptoProvider cannot be used in a synchronous context. Use await constructEventAsync(...) instead of constructEvent(...)

Lovable scaffolded the Stripe webhook Edge Function using the Node.js synchronous constructEvent — but Deno's WebCrypto implementation is async-only, so the synchronous variant throws on first webhook hit.

Fix — paste into Lovable Agent Mode
In the stripe-webhook Edge Function, replace constructEvent with await constructEventAsync. Also read the body as raw text before any verification: const body = await req.text(); const event = await stripe.webhooks.constructEventAsync(body, signature, Deno.env.get('STRIPE_WEBHOOK_SECRET') ?? ''); Do NOT call req.json() anywhere in this function — reading the body as JSON mutates the stream and breaks signature verification.
Webhook signature verification failed

Either the STRIPE_WEBHOOK_SECRET in Cloud Secrets doesn't match the signing secret shown in Stripe Dashboard for your endpoint, or the request body was JSON-parsed before constructEventAsync received it.

Manual fix

In Stripe Dashboard → Developers → Webhooks → click your endpoint → Signing secret (reveal) → copy the whsec_... value → Cloud tab → Secrets → update STRIPE_WEBHOOK_SECRET. Also confirm the Edge Function reads body with req.text() not req.json() before the verification call.

403 / code 42501 when buyer tries to view listing that worked in preview

Preview uses Supabase's service-role key (bypasses RLS); production uses the anon key (respects RLS). Your public-read SELECT policy on listings either doesn't exist for the anon role, or includes an auth.uid() IS NOT NULL check that blocks unauthenticated browsing.

Fix — paste into Lovable Agent Mode
List every RLS policy on listings. Make sure there is a SELECT policy for the anon role with USING clause: status='active' AND is_deleted=false. If any policy has auth.uid() IS NOT NULL, that's the bug — anonymous visitors must be able to browse. Drop the broken policy and recreate: CREATE POLICY public_read_active ON listings FOR SELECT TO anon, authenticated USING (status='active' AND is_deleted=false);
No such file or directory when buyer loads listing image

Listing image URLs were generated against the wrong Storage bucket name, the bucket is private but you used a public URL pattern, or the storage_path column contains the full URL instead of just the file key.

Fix — paste into Lovable Agent Mode
Standardize image access: if the listing-images bucket is public, use supabase.storage.from('listing-images').getPublicUrl(storage_path).data.publicUrl. Confirm storage_path in listing_images only stores the file key (e.g., 'seller-uuid/filename.jpg'), not the full URL. Update ResumeUploader and SellerListings upload handler to write only the key.
Seller can see all orders, not just their own

Naive RLS policy WHERE listing_id IN (SELECT id FROM listings WHERE seller_id = auth.uid()) works but can leak when combined with the public-read policy on listings — the subquery may not be scoped correctly.

Fix — paste into Lovable Agent Mode
Drop the existing seller SELECT policy on orders. Create the SECURITY DEFINER plpgsql function can_see_order(p_order_id uuid) that returns true if auth.uid() equals the buyer_id OR the seller_id of the listing joined to that order. Use LANGUAGE plpgsql not LANGUAGE sql. Create new policy: CREATE POLICY seller_read_own_orders ON orders FOR SELECT TO authenticated USING (can_see_order(id));
Stripe Connect onboarding returns to app but payouts_enabled is still false

Stripe does not set a query parameter on the return redirect to tell you KYC is complete — you must call stripe.accounts.retrieve(account_id) after the seller lands back on your app and check payouts_enabled and charges_enabled on the account object.

Fix — paste into Lovable Agent Mode
On /seller/dashboard mount, if profiles.stripe_account_id exists but payouts_enabled is false, call the refresh-connect-status Edge Function (see follow-up #6). It retrieves the Stripe Account and updates profiles.payouts_enabled. Show a loading spinner while this is in flight on first dashboard load after the seller returns from Stripe KYC.

Cost reality

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

Recommended Lovable plan

Pro $25/mo plus 2-3 top-ups ($15 per 50 credits each). The webhook + RLS + image storage + Connect iteration cycles burn credits fast on a category with no documented Lovable full-marketplace case study to copy from.

Monthly run cost breakdown

~250-500+ credits (starter ~80-120, six follow-ups ~325, plus 50-150 extra for debugging cycles on raw-body webhook and RLS leaks — use '~' prefixes generously, this category varies most) total
ItemCost
Lovable Pro

Drop after build — app runs on Lovable Cloud even on Free plan

$25/mo
Lovable Cloud (Supabase)

Free tier: 500MB DB, 1GB Storage (~500 listings with 3 images each ≈ 600MB — tight), 50K MAU

$0/mo at MVP
Supabase Pro (when needed)

Needed at ~500 listings with images; upgrade before Storage hits 1GB

$25/mo
Stripe Connect fees

At $10K monthly GMV with 10% platform fee, you net ~$680 after Stripe fees

2.9% + 30¢ per charge + 0.25% transfer fee
Custom domain

Required for Stripe Connect production mode (Stripe requires verified domain)

~$10-15/yr

Scaling notes: Storage 1GB free cap at ~500 listings with 2MB images × 3 each. DB 500MB allows ~50K orders. Stripe Connect fees are the biggest ongoing cost — plan your commission rate knowing you'll pay 2.9% + $0.30 per transaction plus the 0.25% Connect transfer fee on top of your platform commission.

Production checklist

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

Domain & SSL

  • Register a custom domain before going live — Stripe Connect production mode requires it

    Buy a domain on Cloudflare or Porkbun. In Lovable top-right → Publish → Custom domain, enter the domain and copy the CNAME record. Add it to your DNS. Then in Stripe Dashboard → Settings → Connect → Branding, set your platform URL to the custom domain.

Stripe webhook verification

  • Test the webhook with Stripe CLI before going live with real cards

    Install the Stripe CLI and run: stripe listen --forward-to [your-edge-function-url]/stripe-webhook. Then trigger a test event: stripe trigger checkout.session.completed. Confirm the order row appears in the Database tab. Fix any issues before switching to production mode.

Backups

  • Enable daily DB backups and confirm Storage backup strategy before first real transaction

    Lovable Cloud (Supabase Free tier) has daily automated backups with 7-day retention. For a marketplace handling real money, consider migrating to your own Supabase Pro project ($25/mo) which adds point-in-time recovery to 5 minutes. Storage is not backed up automatically — export listings data weekly.

Monitoring

  • Watch Edge Function logs for webhook failures daily during first week

    Cloud tab → Edge Functions → stripe-webhook → Logs. Any 4xx from signature failure shows here. Set up Stripe webhook email alerts in Dashboard → Developers → Webhooks → your endpoint → Alerts to get notified if the endpoint fails consistently.

Frequently asked questions

How is a marketplace different from a shopping cart in Lovable?

A shopping cart (single seller) uses one Stripe account — you charge buyers and keep 100% of revenue. A marketplace (multiple sellers) requires Stripe Connect, where each seller has their own Stripe Express account and Stripe splits the payout between the seller and your platform automatically. Connect adds the KYC onboarding step, the connect-onboarding-link Edge Function, and the application_fee_amount parameter to the Checkout session. It also means your webhook must handle payouts rows, not just orders rows.

Do I really need Stripe Connect or can I use a single Stripe account for all sellers?

Legally, you need Stripe Connect if you're collecting money on behalf of third-party sellers. Using a single Stripe account and manually paying sellers via bank transfer works at very small scale (2-5 sellers, informal arrangement) but becomes legally complicated as you grow — you're acting as a payment facilitator without being licensed as one. Stripe Connect's Express accounts solve this by making each seller a direct Stripe customer responsible for their own tax reporting.

What's the cheapest legal way to take payments on day one?

If you're still validating and have 1-3 known sellers: use Stripe Connect in test mode, limit to invited sellers only, and process real payments only after you've tested every edge case with test cards. The overhead of Connect is mostly one-time (KYC per seller) — the ongoing fee (0.25% Connect transfer fee) is small. Do not use a single Stripe account for multi-seller payments at any scale.

How do I prevent fake listings or duplicate sellers?

Stripe Connect itself validates seller identity via KYC (ID + bank account) — you can't activate a seller's listings (set payouts_enabled) until Stripe approves them. For listing quality, the admin moderation queue in follow-up #5 lets you remove flagged content. For duplicate accounts, Stripe detects duplicate bank accounts during Connect onboarding and flags them automatically.

Can I use Lovable Cloud storage for listing images at production scale?

Yes, up to ~500 listings with 3 images each at 2MB per image before hitting the 1GB free tier cap. At that point, upgrade to Supabase Pro ($25/mo) for 100GB Storage — or use Cloudflare R2 ($0.015/GB stored, $0 egress) via an Edge Function for uploads. R2 is the better choice long-term for a public marketplace because there are no egress fees, which matter when images are served on every page load.

What happens when two buyers try to buy the same 1-of-1 item simultaneously?

Without inventory handling, both checkouts succeed and you oversell. Fix this in the stripe-checkout-session Edge Function: before creating the Stripe session, run UPDATE listings SET status='sold' WHERE id=$1 AND status='active' RETURNING id — if 0 rows are returned, throw a 409 error with message 'This item was just purchased by someone else.' This is an atomic check-and-update that prevents the race condition. The buyer sees a 'sold' message before Stripe ever charges them.

When does it become cheaper to pay Sharetribe than to keep iterating in Lovable?

If you need a marketplace live within a week and you're still validating demand, Sharetribe Hard Start at $99/mo is cheaper than the Lovable credits and time you'll spend on the first iteration. Build in Lovable when you have a specific data model or UX flow Sharetribe cannot support, or when you've validated demand on Sharetribe and want to take 100% ownership. 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.