Building a Marketplace App on Lovable
A two-sided marketplace — buyers, sellers, and a platform taking a cut — is one of the most structurally complex apps you can build. Lovable scaffolds the listing and checkout UI fast, but the hard parts are multi-tenant data isolation across two distinct roles, Stripe Connect for split payments, a trust-and-safety model for reviews and disputes, and the policy layer that stops one party from reading another's private transaction data.
By Founder Name · Last verified: 2026-06-25
What does a typical Lovable marketplace scaffold look like?
A Lovable marketplace usually starts with a listings table (seller-owned), a transactions table (buyer to seller), a reviews table, and a Stripe Checkout session. Lovable generates the listing UI, a search and filter page, and the buyer checkout flow well. The gap is structural: it emits direct Stripe Charges rather than Stripe Connect, so the platform cannot take a fee or hold funds — which a real marketplace must do.
The second early gap is the trust model. Lovable produces a reviews table and a star-rating widget, but the rules for who may review (only a verified buyer of a completed transaction), whether reviews can be edited, and how disputes resolve are not encoded by default. Those rules belong in database constraints and RLS policies, not in UI checks a determined user can bypass by calling the API directly.
The third gap is the one that causes incidents: a marketplace has two distinct tenant roles — buyer and seller — over the same rows. Most Lovable scaffolds reuse the single-table pattern auth.uid() = user_id, which has no concept of who is party to a given transaction. That assumption holds in a one-user demo and breaks the moment two real accounts query the same table.
Related: Lovable production-readiness checklist · The 5 production gaps in Lovable apps
Why is multi-tenant data isolation the riskiest part of a marketplace?
Because a marketplace has two tenant roles — buyer and seller — sharing the same tables, and each row has two legitimate owners and many illegitimate viewers. A naive policy that grants broad SELECT on transactions for a dashboard query will leak a competitor's order history, payout amounts, and customer contacts. This is the failure mode that turns a launch into a breach disclosure, and it does not surface until you have multiple real accounts.
Concretely: a seller dashboard that runs select * from transactions and filters in the frontend looks correct in the editor preview, where you are the only user. With a permissive RLS policy behind it, every seller can read every other seller's revenue. The frontend filter is cosmetic; the database is the only enforcement boundary that matters, and RLS is how you set it.
The fix is transaction-scoped policies that resolve the caller's role per row, plus testing each policy with two separate authenticated accounts. Lovable enables RLS on tables it creates but rarely writes the role-aware policies a marketplace needs without explicit prompting — and even prompted policies should be audited by someone who understands Postgres policy evaluation order.
| Table | Naive Lovable policy | What it leaks | Correct scoping |
|---|---|---|---|
| listings | auth.uid() = seller_id for all ops | Drafts and unpublished prices to buyers | Public SELECT on status='active'; write limited to owning seller_id |
| transactions | Broad SELECT for dashboards | Other sellers' orders, payouts, buyer contacts | SELECT only where auth.uid() IN (buyer_id, seller_id) |
| reviews | auth.uid() = reviewer_id | Ability to review without a completed purchase | INSERT gated by a completed transaction FK; SELECT public |
| disputes | auth.uid() = opened_by | The counterparty cannot see a dispute against them | SELECT where auth.uid() IN (buyer_id, seller_id) via transaction join |
| payouts | Not scoped, service-role read | Seller A sees Seller B balances if exposed client-side | Never client-readable; compute server-side in an edge function |
Related: Fix Lovable Supabase RLS and permission errors · When users can see each other's data
How do you write transaction-scoped RLS policies for two roles?
Scope every policy to the caller's relationship to the row, not to a single owner column. A transaction has a buyer and a seller; a dispute is visible to both parties; a payout is visible to neither from the client. The pattern is auth.uid() IN (buyer_id, seller_id) on transactions, and a join through transactions for dependent tables like reviews and disputes so the same two-party rule propagates.
- On transactions, write a SELECT policy: using (auth.uid() = buyer_id OR auth.uid() = seller_id). Add separate INSERT and UPDATE policies so a buyer cannot flip a transaction to 'completed' to unlock a review.
- On reviews, gate INSERT on a completed transaction the reviewer was party to: using (EXISTS (SELECT 1 FROM transactions t WHERE t.id = reviews.transaction_id AND t.status = 'completed' AND t.buyer_id = auth.uid())).
- On disputes, derive visibility by joining to the parent transaction so both parties see it: using (EXISTS (SELECT 1 FROM transactions t WHERE t.id = disputes.transaction_id AND auth.uid() IN (t.buyer_id, t.seller_id))).
- Keep payout balances out of any client-readable table. Compute seller earnings in a Supabase edge function that runs with the service role and returns only the calling seller's figures.
- For every policy, run the query your frontend actually sends as Seller A, then as Seller B, and confirm zero cross-account rows before shipping.
Related: Lovable RLS and auth best practices · Fix infinite recursion in an RLS policy
How do you set up Stripe Connect for a Lovable marketplace?
Stripe Connect is the correct architecture: the platform holds a Connect account, sellers onboard as connected accounts via account links, and payments flow through the platform with an application fee. Lovable does not scaffold Connect — it emits standard Checkout sessions that pay a single recipient. Migrating adds seller onboarding and changes the payment-intent shape, typically a half-day to one-day engineering task once the data model is ready.
- Create a Stripe Connect platform account, then choose Express accounts so sellers see only their earnings, not your full Stripe dashboard.
- Add a seller onboarding flow with stripe.accountLinks.create() against each seller's connected-account ID, and store the resulting account ID on a sellers table.
- Replace stripe.checkout.sessions.create() calls with sessions carrying payment_intent_data: { application_fee_amount, transfer_data: { destination: sellerStripeAccountId } }.
- Handle the account.updated webhook to track onboarding completion, and transfer.created to confirm payouts; refuse checkout for any seller whose onboarding is incomplete.
- Verify the stripe-signature header and dedupe by event ID before processing, so a redelivered event cannot double-credit a payout.
Related: Fix a Stripe webhook that is not firing · Avoid Stripe payment-provider lock-in
What data model does a production marketplace need?
A production marketplace needs explicit relationships with cascade and soft-delete behavior defined. Core tables: listings(seller_id, status, price_cents), transactions(buyer_id, seller_id, listing_id, stripe_payment_intent_id, status), reviews(reviewer_id, reviewee_id, transaction_id, rating), and disputes(transaction_id, opened_by, status, resolution). Every foreign key needs an index, every ON DELETE needs a defined behavior, and every table that holds user data needs a role-aware RLS policy rather than the default single-owner check.
Soft-delete matters more in a marketplace than elsewhere: you cannot hard-delete a seller who still has open transactions, outstanding payouts, or unresolved disputes without orphaning financial records. Use deleted_at TIMESTAMPTZ on sellers, listings, and reviews, and exclude soft-deleted rows in your RLS SELECT policies so they vanish from the UI without breaking referential integrity.
Index every column that appears in a WHERE clause over user data — seller_id, buyer_id, status, listing_id — or seller dashboards degrade as transaction volume grows. Lovable's generated schema commonly omits these indexes and leaves ON DELETE at the default NO ACTION, which produces obscure errors the first time a seller tries to close their account.
Related: Fix Lovable Supabase errors · Lovable app crashes under load
How do you handle trust and safety in a Lovable marketplace?
Trust and safety means verified-purchase reviews, dispute resolution with a payout hold, and a moderation flag on listings and profiles. None of this is scaffolded by Lovable. It requires database triggers, edge functions, and policy decisions you should make before the first real transaction — not after the first dispute, when money has already moved and you have no hold to reverse.
A minimum viable trust layer for launch: a reviewed_transaction_id constraint so a review must reference a completed transaction the reviewer was party to; a payout_hold_days setting (typically 3-7 days after delivery confirmation) before funds transfer to the seller; and a reports table with a pending / reviewed / actioned workflow so you can moderate content without building a full admin console on day one.
The payout hold is the single most valuable control. It gives you a window to freeze or reverse a transfer when a buyer disputes, before the money leaves your platform balance. Without it, your only recourse after a fraudulent sale is chasing a payout that has already settled to the seller's bank.
Related: The 5 production gaps in Lovable apps · Is your Lovable app secure? A checklist
What does Lovable handle well in a marketplace vs what needs hardening?
Lovable is strong at the listing UI, search and filter pages, and the buyer checkout flow. It is weaker at the payment split (Connect), the policy layer (who can see which transaction rows), and the operational tooling (dispute admin, seller analytics). The browse and listing experience can often ship with light review; the payment and data-isolation layers almost always need a senior engineer before real money moves.
| Area | Lovable handles | Needs hardening before launch |
|---|---|---|
| Listing UI | Create, edit, delete listing pages | Image size limits, status workflow, per-listing SEO metadata |
| Search/browse | Filter and sort UI | Full-text index (Postgres tsvector), pagination performance at scale |
| Payments | Stripe Checkout for the buyer | Stripe Connect split, application fee, seller onboarding, webhook dedupe |
| Reviews | Star-rating UI and display | Verified-purchase constraint, edit/delete policy, aggregate score |
| Disputes | Not scaffolded | Dispute table, payout hold, resolution workflow, admin interface |
| Data access | RLS enabled per table | Two-role transaction-scoped policies tested with buyer and seller accounts |
| Scale | Works for a handful of demo users | Index coverage, connection pooling, search performance, soft-delete integrity |
Related: Productionize your Lovable app · Lovable production-readiness checklist
How does a marketplace scale once real volume arrives?
Marketplace load is asymmetric: browse traffic is read-heavy and unauthenticated, while transactions are write-heavy and contended. The first bottlenecks are unindexed seller and status filters, naive LIKE search instead of a tsvector full-text index, and connection exhaustion from per-request Supabase clients. None of these appear in the editor preview; they appear the week a listing gets traction and concurrent buyers hit the same seller.
Add a generated tsvector column with a GIN index for search rather than ILIKE scans, which table-scan every listing. Paginate with keyset (WHERE created_at < cursor) instead of OFFSET, which slows linearly as users page deeper. Move payout and earnings aggregation into edge functions so a heavy dashboard query cannot lock rows the checkout path needs.
Watch for hot rows: a popular seller's row in a counts or ratings table becomes a contention point under concurrent reviews and orders. Aggregate ratings with a periodic job or a trigger that updates a denormalized score, rather than recomputing AVG(rating) on every listing page load.
Related: Lovable app crashes under load · Scale and productionize: the production hub
When is a Lovable marketplace ready for real transactions?
It is ready for real money when Stripe Connect is configured with working seller onboarding; the payout flow is tested end-to-end in Stripe test mode including a failed payment and a refund; RLS policies are verified with separate buyer and seller accounts; a payout hold and dispute path exist; and you can freeze or cancel a transaction without touching the database directly. A senior review usually catches two or three more gaps.
| Check | Pass condition | How to verify |
|---|---|---|
| Data isolation | No cross-account rows under real queries | Query each table as Seller A then Seller B; expect zero leakage |
| Payment split | Application fee and seller transfer both land | Run a test-mode sale; confirm platform fee and connected-account payout |
| Payout hold | Funds held N days before transfer | Trigger a sale, confirm the transfer is delayed and reversible |
| Dispute path | A transaction can be frozen and refunded | Open a dispute, hold payout, issue a partial refund via Stripe |
| Failure handling | Failed payment leaves no half-state | Force payment_intent.payment_failed; confirm no orphaned transaction |
Related: Get a Lovable security audit · Book a productionization call
Frequently asked questions
Does Lovable generate Stripe Connect for marketplaces?
Why is multi-tenant data isolation harder in a marketplace than a normal app?
How do I prevent sellers from seeing other sellers' transaction data?
What is the minimum trust-and-safety setup before accepting real money?
How do I avoid infinite recursion in marketplace RLS policies?
Can I use Supabase Storage for marketplace listing images?
How does a Lovable marketplace handle scale once a listing gets traction?
Should I migrate a Lovable marketplace off Lovable before launch?
Can a non-developer launch a Lovable marketplace with real payments?
Talk to a senior engineer — not a salesperson.
Book a free 30-minute audit call. We'll diagnose what's wrong and tell you exactly what it costs to fix.