TL;DR
The one-paragraph version before you dive in.
Paste the starter prompt below into Lovable Build mode and get a timed-auction marketplace scaffold with live bid feeds, a server-side bid validation RPC using SELECT FOR UPDATE to prevent lost-update bugs, and a Stripe charge-on-close flow. Full build takes a few days and around 200-350 credits on a Pro plan with top-ups.
Setup checklist
Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.
Cloud tab settings
Database
Stores auctions, bids, profiles, and payments. Realtime must be enabled on auctions and bids tables for the live bid feed.
- 1Click the + button next to Preview in the top toolbar to open the Cloud tab
- 2Click Database — the starter prompt will create all tables and the place_bid RPC via migration
- 3After the starter prompt runs, go to Cloud tab → Database → Replication and toggle Realtime ON for the auctions and bids tables
Auth
Email+password for bidders. Admin/seller role stored in app_metadata for moderator queue gating.
- 1Cloud tab → Users & Auth
- 2Enable Email sign-in (default — already on)
- 3To make a user a seller or admin: click their row in the Users list → Edit → set app_metadata to {"role": "admin"}
Storage
Public bucket for auction item photos uploaded during listing creation.
- 1Cloud tab → Storage
- 2Click Create bucket, name it auction-images, toggle Public ON (item photos are publicly visible)
- 3Note the bucket name — the starter prompt references it for the image upload flow in /sell
Edge Functions
Four functions handle bid validation, auction closure scheduling, Stripe charge-on-close, and soft-close extension.
- 1Cloud tab → Edge Functions — leave empty for now; the follow-up prompts will generate each function
- 2For the close-auction function: after it's generated, click Schedule and set it to run every 60 seconds
- 3Add STRIPE_SECRET_KEY and STRIPE_WEBHOOK_SECRET to Cloud tab → Secrets before running the charge-winner follow-up
Secrets (Cloud tab → Secrets)
STRIPE_SECRET_KEYPurpose: Allows charge-winner Edge Function to create Stripe PaymentIntents for auction winners.
Where to get it: https://dashboard.stripe.com/apikeys — copy the Secret key (sk_test_... for testing, sk_live_... for production). In Lovable, use the API Key icon in the chat input or paste directly in Cloud tab → Secrets.
STRIPE_WEBHOOK_SECRETPurpose: Verifies that incoming webhook events actually came from Stripe, not a third party.
Where to get it: https://dashboard.stripe.com/webhooks — after creating the webhook endpoint pointing to your deployed app, click the endpoint row and copy the Signing secret (whsec_...).
RESEND_API_KEYPurpose: Optional — sends winner payment-link emails when a Stripe customer ID is missing.
Where to get it: https://resend.com/api-keys — create a key with Send access.
Preflight checklist
- You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded — that's the default)
- You're on Pro $25/mo plan and budget 1-2 top-ups (~$15 per 50 credits) — bid-validation RPC and Stripe wiring are the most credit-intensive parts of this build
- You understand this is an advanced build — the bid contention, Realtime subscription, and Stripe charge-on-close chain have more moving parts than a typical CRUD app
- Credit estimates for this kit are extrapolated from marketplace + chat-application + payment-gateway cousin patterns — there is no documented exact-match Lovable auction build to triangulate against, so treat all numbers as ranges
The starter prompt — paste this first
Copy this. Paste it into Lovable Agent Mode (the default chat at the bottom-left of the editor). Hit send.
Build an auction platform marketplace. Stack assumptions: React + Vite + TypeScript + Tailwind + shadcn/ui already scaffolded by Lovable, React Router for routes, Supabase JS client for data access against Lovable Cloud.
## Database schema (create one migration file)
Create four tables in the public schema:
1. `auctions` — id (uuid pk default gen_random_uuid()), seller_id (uuid not null references auth.users(id)), title (text not null), description (text), image_path (text), start_price_cents (int not null), current_bid_cents (int not null default 0), current_bidder_id (uuid references auth.users(id)), bid_count (int not null default 0), starts_at (timestamptz not null default now()), ends_at (timestamptz not null), status (text not null default 'live' check (status in ('draft','live','closed','cancelled'))), reserve_cents (int), increment_cents (int not null default 100), soft_close_window_s (int default 60), created_at (timestamptz default now()).
2. `bids` — id (uuid pk default gen_random_uuid()), auction_id (uuid not null references auctions(id) on delete cascade), bidder_id (uuid not null references auth.users(id)), amount_cents (int not null), placed_at (timestamptz default now()), client_request_id (uuid) — idempotency key.
3. `profiles` — id (uuid pk references auth.users(id) on delete cascade), display_name (text), avatar_url (text), stripe_customer_id (text), created_at (timestamptz default now()).
4. `payments` — id (uuid pk default gen_random_uuid()), auction_id (uuid references auctions(id)), winner_id (uuid references auth.users(id)), stripe_payment_intent_id (text), amount_cents (int), status (text not null default 'pending' check (status in ('pending','succeeded','failed','refunded'))), created_at (timestamptz default now()).
Also create a view `winning_bids` as: SELECT a.id as auction_id, a.title, a.current_bidder_id, a.current_bid_cents, a.ends_at FROM auctions a WHERE a.status = 'closed'.
## RLS policies
Enable RLS on all four tables.
- auctions: SELECT for anyone where status = 'live'; INSERT for authenticated where seller_id = auth.uid(); UPDATE only via RPC (no direct update policy for users).
- bids: SELECT for anyone; no direct INSERT policy (insert only via place_bid RPC).
- profiles: SELECT for anyone (display_name only); INSERT/UPDATE for auth.uid() = id.
- payments: SELECT for authenticated where winner_id = auth.uid() OR has_role('admin'); no INSERT/UPDATE for users.
Create a SECURITY DEFINER plpgsql function `has_role(p_role text)` that returns boolean: `RETURN (auth.jwt() ->> 'app_metadata')::jsonb ->> 'role' = p_role;`
## place_bid RPC (the heart of the auction)
Create a SECURITY DEFINER plpgsql function `place_bid(p_auction_id uuid, p_amount_cents integer, p_client_request_id uuid DEFAULT NULL)` that:
1. Runs in a transaction with SELECT ... FOR UPDATE on the auctions row (to lock it).
2. Validates that status = 'live' and now() < ends_at — raises exception 'Auction is not accepting bids' if not.
3. Validates that p_amount_cents >= current_bid_cents + increment_cents — raises exception 'Bid must be at least $X.XX' if not (format the minimum nicely).
4. Rejects if auth.uid() = seller_id (anti-shilling).
5. Optionally checks for duplicate client_request_id in bids (idempotency).
6. Handles soft-close: if (ends_at - now()) < soft_close_window_s seconds, extend ends_at by soft_close_window_s.
7. INSERTs into bids (auction_id, bidder_id, amount_cents, client_request_id) and UPDATEs auctions SET current_bid_cents = p_amount_cents, current_bidder_id = auth.uid(), bid_count = bid_count + 1, ends_at = (extended if soft-close triggered) in one step.
8. Returns the new current_bid_cents and the updated ends_at.
Grant EXECUTE on place_bid to authenticated.
## Layout and pages
Create `src/layouts/PublicLayout.tsx` — top nav with logo, 'Live Auctions' link, search input, user avatar (shadcn Avatar + DropdownMenu with Sign In / My Account / Sign Out). Mobile-friendly hamburger for nav.
Create these routes in src/App.tsx:
- / (Home.tsx) — grid of live auctions, filtered by status='live', sorted by ends_at ASC. Each item = AuctionCard component.
- /auctions/:id (AuctionDetail.tsx) — shows auction image, title, description, CountdownTimer (re-renders every second from ends_at, red when <60s), HighBidBadge (shows current bid + bidder display_name; if current_bidder_id = auth.uid() shows 'You are winning'), BidInput (min-increment validation in UI as UX only), BidHistoryList (last 20 bids). All three use Supabase Realtime subscriptions on auctions:id=eq.{id} and bids:auction_id=eq.{id}.
- /sell (Sell.tsx) — create auction form: title, description, image upload to auction-images bucket, start_price, increment_cents, reserve_cents optional, ends_at datetime picker. Auth-gated.
- /account (Account.tsx) — tabs: My Bids (bids placed, with auction status), My Listings (seller's auctions), Won Items (winning_bids view where current_bidder_id = me, with Pay Now button for pending payments).
- /admin/moderation (Moderation.tsx) — admin-only (has_role('admin')), list of all auctions with status controls.
- /login (Login.tsx) — email+password form.
## Key components
Create: AuctionCard (image + title + current bid + countdown badge + status chip), CountdownTimer (accepts ends_at prop, updates every second, shows HH:MM:SS, turns text-red-500 when <60s), BidInput (shows minimum bid, calls place_bid RPC on submit, shows error from RPC clearly, shows 'Extended!' toast when returned ends_at > previous ends_at), BidHistoryList (Realtime-subscribed list of latest bids with display_name + amount + time), HighBidBadge (green if auth.uid() = current_bidder_id, otherwise shows leader's display_name).
## Styling
Dark mode as default (className='dark' on html). Tailwind dark: variants throughout. Accent color amber-500 for live bid CTAs, bid amounts, and countdown when active. Monospace font for countdown digits (font-mono). Subtle ring-2 ring-amber-400 pulse animation on BidInput when a new bid arrives via Realtime.
Generate the migration file first, then the place_bid RPC, then PublicLayout, then pages in the order listed, then components. After the migration is generated, pause and confirm before proceeding.What this prompt generates
- Creates one SQL migration with 4 tables, RLS policies, has_role() function, place_bid RPC with SELECT FOR UPDATE, soft-close logic, and winning_bids view
- Scaffolds PublicLayout with responsive top nav and auth-aware user menu
- Generates Home grid, AuctionDetail with working Realtime bid feed and countdown timer, /sell creation form, and /account dashboard
- Builds all five core components: AuctionCard, CountdownTimer, BidInput (calling place_bid RPC), BidHistoryList, HighBidBadge
- Wires admin moderation queue gated by has_role('admin')
Paste into: Lovable Agent 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_auction_schema.sql4 tables + RLS + has_role() + place_bid RPC + winning_bids view
src/layouts/PublicLayout.tsxTop nav with logo, search, user avatar dropdown
src/components/AuthGuard.tsxRedirects to /login if user not authenticated
src/components/AuctionCard.tsxGrid card with image, title, current bid, countdown badge
src/components/CountdownTimer.tsxLive countdown from ends_at, turns red under 60s
src/components/BidInput.tsxCalls place_bid RPC, shows RPC errors, Extended! toast on soft-close
src/components/BidHistoryList.tsxRealtime-subscribed list of recent bids
src/components/HighBidBadge.tsxGreen You are winning badge or shows leader display_name
src/components/ImageUploader.tsxDrag-drop upload to auction-images Storage bucket
src/pages/Home.tsxLive auctions grid sorted by ends_at
src/pages/AuctionDetail.tsxDetail view with Realtime bid feed, countdown, bid input
src/pages/Sell.tsxAuction creation form with image upload
src/pages/Account.tsxMy Bids, My Listings, Won Items tabs
src/pages/admin/Moderation.tsxAdmin-only auction moderation queue
Routes
Live auctions grid
Auction detail with Realtime bid feed and countdown
Create new auction — auth-gated
Bidder/seller account — bids, listings, won items
Admin auction moderation queue
Email+password sign-in
DB Tables
auctions| Column | Type |
|---|---|
| id | uuid primary key |
| seller_id | uuid references auth.users(id) |
| title | text not null |
| current_bid_cents | int not null default 0 |
| current_bidder_id | uuid references auth.users(id) |
| ends_at | timestamptz not null |
| status | text check in (draft,live,closed,cancelled) |
| increment_cents | int not null default 100 |
| soft_close_window_s | int default 60 |
RLS: Public SELECT for status=live; INSERT for seller only; UPDATE only via place_bid RPC
bids| Column | Type |
|---|---|
| id | uuid primary key |
| auction_id | uuid references auctions(id) on delete cascade |
| bidder_id | uuid references auth.users(id) |
| amount_cents | int not null |
| placed_at | timestamptz default now() |
| client_request_id | uuid |
RLS: Public SELECT; INSERT only via place_bid RPC — no direct client INSERT policy
profiles| Column | Type |
|---|---|
| id | uuid primary key references auth.users(id) |
| display_name | text |
| avatar_url | text |
| stripe_customer_id | text |
RLS: Public SELECT for display_name; per-user write of own row
payments| Column | Type |
|---|---|
| id | uuid primary key |
| auction_id | uuid references auctions(id) |
| winner_id | uuid references auth.users(id) |
| stripe_payment_intent_id | text |
| amount_cents | int |
| status | text check in (pending,succeeded,failed,refunded) |
RLS: SELECT for winner_id = auth.uid() OR has_role('admin'); no user INSERT
Components
AuctionCardGrid item with image, current bid, countdown badge, status chip
CountdownTimerLive countdown that turns red under 60 seconds
BidInputValidates minimum increment, calls place_bid RPC, surfaces RPC errors
BidHistoryListRealtime-subscribed ordered list of recent bids
HighBidBadgeShows You are winning (green) or leader display name
ImageUploaderDrag-drop upload to auction-images Storage bucket
Follow-up prompts
Paste these into Agent Mode one by one, in order, after the starter prompt finishes.
Enable Realtime on auctions and bids tables
Live bid feed so every spectator sees the new high bid within ~1 second without refreshing
Enable Supabase Realtime on the auctions and bids tables. In AuctionDetail.tsx, add two Realtime subscriptions inside a single useEffect:
1. Subscribe to channel 'auction-{id}' filtering on auctions:id=eq.{auctionId}. On UPDATE event, update the local auction state with the new current_bid_cents, current_bidder_id, bid_count, and ends_at.
2. Subscribe to channel 'bids-{id}' filtering on bids:auction_id=eq.{auctionId}. On INSERT event, prepend the new bid to BidHistoryList and update the HighBidBadge.
Also add a re-subscribe on document.visibilitychange so the subscriptions reconnect when the user returns to the tab after backgrounding the browser. Add a 30-second polling fallback: if no Realtime event has been received in 30s, fetch the latest 5 bids and the current auction state directly from Supabase. Clean up all subscriptions on component unmount.When to use: Once the starter prompt finishes and you verify place_bid writes work in two browser tabs
Schedule auction closure with close-auction Edge Function
Server-controlled auction closure so auctions end at exactly ends_at regardless of whether any user is watching
Create a Supabase Edge Function at supabase/functions/close-auction/index.ts. This function runs on a schedule every 60 seconds.
The function should:
1. Query auctions where ends_at < now() AND status = 'live'.
2. For each, set status = 'closed' in a single UPDATE.
3. For each closed auction where current_bidder_id IS NOT NULL (there was a winner), INSERT a row into payments (auction_id, winner_id = current_bidder_id, amount_cents = current_bid_cents, status = 'pending').
4. If there was no winner (current_bidder_id IS NULL), skip the payments insert — just close the auction.
5. After writing all payments rows, invoke the charge-winner Edge Function for each payment row (pass payment_id as the body). Use the Supabase JS client with the service-role key to call the function.
6. Return a summary JSON: {closed_count, payments_created}.
This function requires the service-role key — use Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') (Lovable Cloud auto-injects this). After generating, go to Cloud tab → Edge Functions → close-auction → Schedule and set it to run every 60 seconds.When to use: Once live bidding and Realtime work; this is required before going live to real bidders
Wire Stripe charge-on-close with charge-winner Edge Function
Automatic winner payment via saved Stripe card, with email fallback for bidders without a saved method
Create a Supabase Edge Function at supabase/functions/charge-winner/index.ts. This function is called by close-auction with a payment_id in the body.
The function should:
1. Read the payment row from the payments table using the provided payment_id.
2. Look up profiles.stripe_customer_id for the winner.
3. If stripe_customer_id exists: create a Stripe PaymentIntent with amount = payment.amount_cents, currency = 'usd', customer = stripe_customer_id, confirm = true (auto-confirm if they have a saved payment method). Update payments.status to 'succeeded' or 'failed' based on the result.
4. If stripe_customer_id is null: send a Resend email to the winner's email address with a Stripe Payment Link URL and a message like 'Congratulations — you won [auction title]! Complete your purchase here: [link]'. Update payments.status to 'pending' with a note.
5. Return {payment_id, status}.
Use STRIPE_SECRET_KEY from Deno.env.get() and RESEND_API_KEY for emails. Never hardcode keys. The function must handle errors gracefully — if Stripe fails, log the error, set status='failed', and return a 200 (so close-auction doesn't retry in a loop).When to use: Once close-auction works; add STRIPE_SECRET_KEY to Cloud Secrets before running this prompt
Add ensure-stripe-customer trigger on profile creation
Stripe customer IDs created at signup so charge-winner always has a customer to charge
Create a Supabase Edge Function at supabase/functions/ensure-stripe-customer/index.ts that accepts a user object (id, email) and creates a Stripe Customer record if one doesn't already exist for that user.
The function should:
1. Check profiles.stripe_customer_id for the given user id — if it already exists, return it immediately.
2. If not, call stripe.customers.create({ email: user.email, metadata: { supabase_user_id: user.id } }).
3. Update profiles.stripe_customer_id = customer.id where id = user.id using the service-role key.
4. Return { stripe_customer_id }.
Then create a Postgres trigger function `on_auth_user_created` that fires AFTER INSERT ON auth.users. The trigger should call ensure-stripe-customer via an HTTP request using net.http_post() (pg_net extension). Also run a one-time backfill: for all profiles where stripe_customer_id IS NULL, call ensure-stripe-customer.
Add a backfill button in /admin/moderation that runs the backfill on demand.When to use: Before going live to real bidders — run the backfill for existing users immediately after deploying
Add anti-shilling fraud checks inside place_bid
Server-side anti-shilling guards that block seller self-bidding and rate-limit bid spam
Update the place_bid Postgres RPC to add three anti-shilling guards: 1. Seller cannot bid on own auction: if auth.uid() = (SELECT seller_id FROM auctions WHERE id = p_auction_id), raise exception 'Sellers cannot bid on their own listings.'. 2. Rate limit: if EXISTS (SELECT 1 FROM bids WHERE bidder_id = auth.uid() AND auction_id = p_auction_id AND placed_at > now() - interval '2 seconds'), raise exception 'Please wait a moment before placing another bid.' 3. Flag for review: if (SELECT count(*) FROM bids b JOIN auctions a ON a.id = b.auction_id WHERE b.bidder_id = auth.uid() AND a.seller_id = (SELECT seller_id FROM auctions WHERE id = p_auction_id)) > 50, INSERT into a new table `fraud_flags` (user_id, reason, created_at) — create this table with RLS admin-only. Do not block the bid, just flag it. Create the fraud_flags table in a new migration with RLS admin-only SELECT and INSERT via place_bid (SECURITY DEFINER bypass). Add a Fraud Flags tab in /admin/moderation.
When to use: Before going live to real bidders — these guards cannot be added retroactively without migrating existing bids
Add seller dashboard with reserve-not-met logic
Seller dashboard with reserve-not-met handling and relist flow for failed auctions
Add a Seller Dashboard section to /account under a My Listings tab. Show each of the seller's auctions with: title, current status, current_bid_cents vs reserve_cents, bid_count, ends_at countdown. For closed auctions where current_bid_cents < reserve_cents: show a yellow 'Reserve not met' badge and a 'Relist' button that creates a new auction pre-filled with the same title/description/image/reserve and a new ends_at. For closed auctions where the reserve was met but payment.status = 'pending': show 'Awaiting payment' with the winner's display_name. For closed auctions where payment.status = 'succeeded': show 'Sold' with the final amount. Update close-auction Edge Function: when closing an auction, if current_bid_cents < reserve_cents, do NOT create a payments row — set status='closed' and log the reserve-not-met outcome in a new column `close_reason` (text, values: 'won','reserve_not_met','no_bids','cancelled') on the auctions table. Add this column in a new migration.
When to use: Once you have repeat sellers or reserve pricing matters for your use case
Add winner notification emails via Resend
Branded winner and seller notification emails on auction close
Add Resend winner notification emails to the charge-winner Edge Function. After processing a payment (whether succeeded or pending), send a branded email to the winner using RESEND_API_KEY from Deno.env.get():
Subject: 'You won: [auction title]!'
Body: Congratulate the winner, show the winning amount, include a link to /account and a link to their Stripe portal (if stripe_customer_id exists).
Also send a seller notification email: 'Your auction [title] has closed. Winning bid: $X.XX. Winner: [display_name].'
Use Resend's Node.js-compatible SDK: import { Resend } from 'https://esm.sh/resend@latest'. Wrap both sends in try/catch — a failed email must never fail the webhook response.
Add RESEND_API_KEY to Cloud tab → Secrets before running this prompt.When to use: Once charge-winner works end-to-end and you want off-brand Stripe emails replaced
Common errors
Real error strings you'll see. Find yours, paste the fix prompt.
Bid was rejected: current bid is now $510 (your bid: $501)Working as designed — your bidder lost the race to another bid. The RPC's SELECT FOR UPDATE lock did its job and rejected the stale bid.
Refresh the BidInput minimum by reading the Realtime update that came in with the new current_bid_cents. Show the user a toast: 'Someone just outbid you — minimum is now $[new_minimum]' and update the input placeholder. Do not treat this as a bug.
infinite recursion detected in policy for relation "bids"You added an RLS policy on bids that joins back to auctions, and the auctions policy in turn queries bids — creating a circular reference that Postgres detects and rejects.
Create a SECURITY DEFINER plpgsql function can_see_bid(p_bid_id uuid) that returns boolean and checks the auction's status directly without triggering RLS on bids. Use this function in the bids SELECT policy: USING (can_see_bid(id)). Do NOT use a SQL-language function — it gets inlined and still recurses.
permission denied for function place_bidThe RPC was created but EXECUTE was not granted to the authenticated role.
Manual fix
Open Cloud tab → Database → SQL Editor and run: GRANT EXECUTE ON FUNCTION place_bid(uuid, integer, uuid) TO authenticated;
Realtime bid feed stops updating after 30 secondsFree tier caps concurrent Realtime connections at 200. If your auction goes viral, additional spectators silently fail to subscribe. Also, Lovable's default useEffect doesn't always re-subscribe after tab focus loss.
In AuctionDetail.tsx, wrap the Realtime subscription in a useEffect that also listens to the document visibilitychange event and re-subscribes when document.visibilityState becomes 'visible'. Add a 30-second polling fallback that fetches the latest 5 bids from Supabase directly if no Realtime event has arrived in that window.
No such customer: cus_xxx when calling charge-winnerThe bidder never had a Stripe customer created at sign-up — the ensure-stripe-customer trigger either wasn't added yet or failed silently for that user.
In the charge-winner Edge Function, before trying to create a PaymentIntent, check if profiles.stripe_customer_id is null. If so, call ensure-stripe-customer inline to create the customer, save the ID, then proceed. Also run the backfill button in /admin/moderation to fix all existing users without a stripe_customer_id.
Auction closed but charge-winner never ranThe close-auction function set status='closed' but did not chain to invoke charge-winner. Edge Functions do not auto-trigger each other.
Inside close-auction, after setting status='closed' and inserting the payments row, add an explicit call using the Supabase JS client: await supabase.functions.invoke('charge-winner', { body: { payment_id: payment.id } }). Use the service-role key for this client so the invocation has full permissions.Cost reality
What this build actually costs — no surprises on your card.
Recommended Lovable plan
Pro $25/mo plus 1-2 top-ups ($15 per 50 credits). The bid-validation RPC and Stripe charge-on-close wiring are the most credit-expensive parts — Stripe debugging alone has been reported to burn a month's Pro credits in an afternoon.
Monthly run cost breakdown
~200-350 credits (starter ~60-100, seven follow-ups ~200-250 if done in full, plus 2-3 rounds of policy and webhook iteration). Treat all estimates as ranges — no documented exact-match Lovable auction build exists to triangulate against. total| Item | Cost |
|---|---|
| Lovable Pro Can drop to Free after launch if you're not iterating | $25/mo |
| Lovable Cloud / Supabase Free Free tier includes 200 concurrent Realtime connections — enough for ~200 simultaneous spectators on one auction | $0/mo at MVP scale |
| Supabase Pro (when needed) Bump to Pro when you outgrow 200 Realtime connections or 500MB DB | $25/mo |
| Stripe Per winning bid settled — no monthly fee | 2.9% + 30¢ per charge |
| Resend Free up to 3,000 emails/month | $0/mo |
| Custom domain | $10-15/yr |
Scaling notes: 200 concurrent Realtime connections is the first ceiling — if a single auction draws more than 200 simultaneous viewers, additional viewers silently fail to subscribe and see a stale bid display. Upgrade to Supabase Pro ($25/mo) for 500 concurrent connections. The 500MB DB is the second ceiling — bids tables grow fast on high-volume auction days. Stripe per-charge fees are unavoidable and scale linearly with settlement volume.
Production checklist
Steps to take before you share the URL with real users.
Domain & SSL
Publish to a custom domain
In Lovable, click the Publish icon (top right) → Settings → Custom Domain → enter your domain and follow the CNAME DNS instructions. SSL is provisioned automatically.
Update Stripe webhook endpoint to production URL
In Stripe Dashboard → Developers → Webhooks → Add endpoint, enter https://yourdomain.com/api/stripe-webhook. Copy the new signing secret to Cloud tab → Secrets as STRIPE_WEBHOOK_SECRET.
Fraud & Security
Test place_bid atomicity with two browser tabs
Open two incognito windows, sign in as two different non-admin users, both bid the same amount simultaneously. Only one should succeed. If both receive receipts, the SELECT FOR UPDATE lock is not working — re-check the RPC.
Verify RLS with a non-admin account
Sign in as a regular bidder (not the Lovable preview account which uses service-role). Attempt a direct Supabase JS call to insert into bids without going through place_bid. Should receive permission denied.
Backups & Monitoring
Enable daily database backups
Cloud tab → Database → Backups — confirm daily backups are enabled. On Supabase Free, backups are retained for ~7 days. Upgrade to Pro for point-in-time recovery.
Set up Stripe fraud protection
In Stripe Dashboard → Radar → Rules, enable the default rules for card testing and high-risk payments. For an auction platform, also add a rule to block cards used on more than 3 failed attempts in 24 hours.
Stripe Go-Live
Switch from test to live Stripe keys
In Stripe Dashboard, activate your account (provide tax ID + bank details). Update STRIPE_SECRET_KEY in Cloud Secrets to sk_live_... and STRIPE_WEBHOOK_SECRET to the live webhook signing secret. Test with a real $1 charge on your own card and immediately refund.
Frequently asked questions
Can I run this on the Free Lovable plan?
You can start on Free for the first few prompts, but the full auction build will exceed the 30-credit monthly cap quickly. The place_bid RPC alone takes 40+ credits to get right, and Stripe wiring adds another 60-80. Plan on Pro $25/mo from the start and budget 1-2 top-ups. The Free plan's 200 concurrent Realtime connections cap is fine for a soft launch.
Why can't I just insert into the bids table directly from React?
Two reasons. First, two browser tabs can send bid requests simultaneously, and without a SELECT FOR UPDATE lock inside a database transaction, both writes can succeed — meaning two bidders both get a receipt claiming they won with the same amount. That is a real money dispute. Second, a determined user can open DevTools, call the Supabase JS client directly, and send any amount they want — bypassing your React validation entirely. The place_bid RPC running inside Postgres with server-enforced rules is the only correct pattern.
What happens if the Stripe charge fails after the auction closes?
The charge-winner function sets payments.status to 'failed' and sends the winner a payment-link email so they can complete the purchase manually via a hosted Stripe checkout. In /account → Won Items, the winner sees a 'Complete payment' button. If they don't pay within 48 hours, you can cancel the sale and relist — add an admin action in /admin/moderation for this. Do not attempt automatic retries for charge failures; Stripe's built-in retry logic handles declined cards on subscriptions but not one-time PaymentIntents.
How do I handle bidder cancellation or non-payment?
There is no automatic non-payment resolution in v1. You need an admin workflow: in /admin/moderation, add a 'Cancel sale + relist' action on any auction in the closed state with a pending payment. This resets the auction status to 'draft' and lets the seller edit and relist. For repeat non-payers, add a blocked_bidders table and check it in place_bid — the anti-shilling follow-up prompt is the right place to add this.
Can I run multiple auctions simultaneously without overwhelming the Realtime tier?
Yes, but watch the connection count. Each auction detail page open by a user consumes 2 Realtime connections (one for the auctions channel, one for the bids channel). The Free Supabase tier allows 200 concurrent connections total across all auctions. If you run 10 simultaneous auctions with 10 viewers each, that's 200 connections — at the cap. Upgrade to Supabase Pro ($25/mo) for 500 connections when your auction volume grows. The 30-second polling fallback in the Realtime follow-up prompt acts as a safety net for spectators who can't get a connection.
Is this legal — do I need an auctioneer license?
Auctioneer licensing requirements vary significantly by jurisdiction. In the United States, most states require a license for live or online auctions of certain goods, especially real property, art, and livestock. Running a closed B2B surplus or charity auction for your own inventory is generally lower-risk. This page builds the software, not legal advice — consult an attorney familiar with auction law in your jurisdiction before opening to the public, especially if you plan to handle high-value goods or take a percentage of sales.
Can RapidDev build a production auction platform for me?
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 consultation30-min call. No commitment.