TL;DR
The one-paragraph version before you dive in.
Paste the starter prompt into Lovable Agent Mode and get a working community forum: categories, threaded posts and comments, upvote and downvote with score triggers, anti-spam rate-limiting, OpenAI Moderation, Supabase Realtime feed, and a moderator dashboard. Full build takes roughly one day. Expected credit burn: 100-180 on a Pro $25/mo plan.
Setup checklist
Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.
Cloud tab settings
Database
Stores 7 tables: profiles, categories, posts, comments, votes, reports, bans. Rate-limit trigger and vote-score triggers live here.
- 1Click the + button next to Preview in the top toolbar to open the Cloud tab
- 2Click Database — you'll see an empty Table Editor
- 3Leave it empty — the starter prompt creates all tables and triggers via migration
Auth
Email+password for members; Google OAuth optional. Moderator role stored in JWT app_metadata so it cannot be tampered with from the client.
- 1Cloud tab → Users & Auth
- 2Under Sign-in methods, enable Email (already on by default); optionally enable Google
- 3Leave 'Allow new users to sign up' ON so members can self-register
- 4Enable email confirmation: scroll to 'Email auth' → 'Confirm email' → toggle ON. This is your first line of spam defense.
- 5After your first moderator account is created, open that user's row and edit app_metadata to {"role": "moderator"}
Edge Functions
moderate-post calls OpenAI Moderation API; notify-mention sends Resend emails on @username mentions.
- 1No manual setup needed before running the starter prompt
- 2After the starter finishes, confirm moderate-post and notify-mention appear in Cloud tab → Edge Functions
- 3Add OPENAI_API_KEY and RESEND_API_KEY in Cloud tab → Secrets before testing moderation
Secrets (Cloud tab → Secrets)
OPENAI_API_KEYPurpose: Used by moderate-post Edge Function to call the OpenAI Moderation endpoint. The Moderation API is effectively free — it's bundled and rarely billed separately.
Where to get it: Go to https://platform.openai.com/api-keys — create a key with 'All' permissions
RESEND_API_KEYPurpose: Sends @mention notification emails and reply alerts to community members
Where to get it: Go to https://resend.com/api-keys — create a key with 'Sending access'
Preflight checklist
- You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded)
- You're on Pro $25/mo — the full chain burns 100-180 credits and Free caps at ~30/mo
- You've enabled email confirmation in Auth settings before testing to prevent throwaway-account spam
- If adding moderation from day one (strongly recommended), add OPENAI_API_KEY to Cloud tab → Secrets before publishing
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 a community forum. Stack: React + Vite + TypeScript + Tailwind + shadcn/ui (already scaffolded), React Router v6 for routes, Supabase JS client against Lovable Cloud.
## Database schema (create one migration)
Create seven tables in the public schema:
1. `profiles` — id uuid PK references auth.users.id, username text unique not null check (length(username) BETWEEN 3 AND 30), display_name text, avatar_url text, bio text, karma int default 0, role text default 'member' check in ('member','moderator','admin'), created_at timestamptz default now(). RLS: public-read; own write; role enforced via JWT app_metadata, not this column.
2. `categories` — id uuid PK, slug text unique not null, name text not null, description text, position int default 0, created_at timestamptz default now(). RLS: public-read; admin-write via has_role().
3. `posts` — id uuid PK default gen_random_uuid(), author_id uuid not null references auth.users.id, category_id uuid not null references categories(id), title text not null check (length(title) BETWEEN 5 AND 200), body text not null check (length(body) BETWEEN 10 AND 10000), is_deleted bool default false, is_locked bool default false, is_pinned bool default false, vote_score int default 0, comment_count int default 0, created_at timestamptz default now(). RLS: public-read WHERE NOT is_deleted; INSERT authenticated WITH CHECK calling posts_rate_limit(); UPDATE/DELETE by author OR moderator/admin.
4. `comments` — id uuid PK, post_id uuid not null references posts(id) on delete cascade, author_id uuid not null references auth.users.id, parent_comment_id uuid references comments(id), body text not null check (length(body) BETWEEN 1 AND 5000), is_deleted bool default false, vote_score int default 0, created_at timestamptz default now(). RLS: public-read WHERE NOT is_deleted AND post not deleted; INSERT authenticated rate-limited.
5. `votes` — id uuid PK, voter_id uuid not null references auth.users.id, target_type text check in ('post','comment'), target_id uuid not null, value smallint check in (-1, 1), created_at timestamptz default now(). UNIQUE(voter_id, target_type, target_id). RLS: voter writes own; public aggregates via denormalized vote_score on post/comment.
6. `reports` — id uuid PK, reporter_id uuid references auth.users.id, target_type text check in ('post','comment','user'), target_id uuid not null, reason text not null, status text default 'open' check in ('open','dismissed','actioned'), reviewed_by uuid references auth.users.id, reviewed_at timestamptz, created_at timestamptz default now(). RLS: reporter writes; moderator reads/updates.
7. `bans` — id uuid PK, user_id uuid references auth.users.id, banned_by uuid references auth.users.id, reason text, expires_at timestamptz, created_at timestamptz default now(). RLS: moderator-only read/write.
## Required triggers and functions
Create these as part of the migration:
A. `has_role(check_role text) RETURNS boolean LANGUAGE plpgsql SECURITY DEFINER STABLE` — reads `(auth.jwt() -> 'app_metadata' ->> 'role') = check_role`.
B. `posts_rate_limit() RETURNS trigger LANGUAGE plpgsql` — BEFORE INSERT on posts. If (SELECT COUNT(*) FROM posts WHERE author_id = NEW.author_id AND created_at > now() - interval '10 minutes') >= 5, RAISE EXCEPTION 'P0001: Rate limit exceeded: max 5 posts per 10 minutes'. Return NEW otherwise.
C. `update_target_vote_score() RETURNS trigger LANGUAGE plpgsql` — AFTER INSERT OR DELETE on votes. Updates posts.vote_score or comments.vote_score with SUM(value) for the target. Use NEW.target_id on INSERT, OLD.target_id on DELETE.
Attach triggers:
- `posts_rate_limit_trigger` BEFORE INSERT ON posts EXECUTE FUNCTION posts_rate_limit()
- `votes_score_trigger` AFTER INSERT OR DELETE ON votes FOR EACH ROW EXECUTE FUNCTION update_target_vote_score()
Also run: `ALTER PUBLICATION supabase_realtime ADD TABLE posts; ALTER PUBLICATION supabase_realtime ADD TABLE comments;`
Seed: 4 categories (General / Q&A / Showcase / Off-topic).
## Layout
Create `src/layouts/PublicLayout.tsx` — top nav with logo, category pill nav (from categories table), search box, sign in button / user avatar dropdown with username + karma + sign out. Mobile: hamburger menu collapses category nav.
## Pages
Create these routes in src/App.tsx:
- `/` (Home.tsx) — sorted feed of all posts (most recent), each row is a PostCard with vote arrows
- `/c/:categorySlug` (Category.tsx) — feed filtered to one category
- `/post/:postId` (PostDetail.tsx) — full post body + CommentThread (nested up to 3 levels)
- `/post/new` (NewPost.tsx) — form with category dropdown + title + body (Markdown textarea); requires auth
- `/u/:username` (UserProfile.tsx) — user profile with their posts and karma score
- `/search` (Search.tsx) — full-text search results via Supabase full-text search on posts.title + body
- `/mod` (ModerationDashboard.tsx) — reports queue (open reports with actions: Dismiss / Delete / Ban Author)
- `/login` and `/signup`
## Components
Build these reusable components:
- `AuthGuard` — wraps /post/new and vote interactions; redirects to /login if no session
- `ModeratorGate` — wraps /mod routes; checks JWT app_metadata.role IN ('moderator','admin')
- `PostCard` — compact row: vote arrows (up/orange, down/blue, unvote on re-click) + title + author username + category badge + comment count + age ('2h ago')
- `CommentThread` — recursive rendering of comments with left-border indentation per nesting level; collapse/expand; Reply button requires auth
- `VoteArrows` — handles optimistic upvote/downvote/unvote; INSERT into votes table; shows user's current vote state
- `ReportDialog` — reason dropdown + submit; creates reports row
- `NewPostForm` — title input + category combobox + body textarea + submit; catches P0001 rate-limit exception and shows friendly toast
## Edge Functions
Scaffold `supabase/functions/moderate-post/index.ts` — accepts {text} (post title + body concatenated), calls OpenAI Moderation endpoint at https://api.openai.com/v1/moderations with OPENAI_API_KEY from secrets, returns {flagged: bool, categories: object}. Client calls this before INSERT; if flagged, still INSERTs but with is_deleted=true and creates a reports row with reason='auto_moderation_flagged'.
Scaffold `supabase/functions/notify-mention/index.ts` — accepts {post_id, comment_id, body, author_username}. Parses body for @username patterns, looks up matching profiles, sends Resend email to each mentioned user with subject '@{author} mentioned you in {post_title}' and a link to the post.
## Styling
Light + dark mode via shadcn ThemeProvider. Primary color orange-500 (community energy). PostCard: no card background — list-style with bottom border, vote arrows left, title right. VoteArrows: orange on upvote, blue/indigo on downvote, gray neutral. Threaded comment indentation: 20px left border per level, max 3 levels deep. Category pills: colored by position (use ring-orange-500, ring-blue-500 etc). Moderator UI: banner at top of /mod with 'X open reports' badge.
Generate migration first, then PublicLayout, then pages in order, then Edge Functions.What this prompt generates
- Creates a SQL migration with 7 tables, rate-limit BEFORE INSERT trigger, vote-score AFTER INSERT trigger, SECURITY DEFINER has_role() function, and Realtime publication for posts and comments
- Scaffolds PublicLayout with category navigation and 8 page components wired to React Router
- Builds PostCard, CommentThread, VoteArrows, and ModeratorGate components with correct vote state handling
- Scaffolds moderate-post Edge Function (OpenAI Moderation) and notify-mention Edge Function (Resend)
- Seeds 4 categories so the feed is immediately usable in preview
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_forum_schema.sql7 tables + RLS + rate-limit trigger + vote-score trigger + has_role() + Realtime publication + seed categories
src/layouts/PublicLayout.tsxTop nav with category pills, search, and user dropdown
src/components/AuthGuard.tsxRedirects unauthenticated users to /login
src/components/ModeratorGate.tsxChecks JWT app_metadata.role IN (moderator, admin)
src/components/PostCard.tsxCompact post row with vote arrows, title, metadata
src/components/CommentThread.tsxRecursive nested comment renderer with collapse/reply
src/components/VoteArrows.tsxOptimistic upvote/downvote handler with user vote state
src/components/ReportDialog.tsxReport reason form that creates reports rows
src/components/NewPostForm.tsxCreate post form with rate-limit error handling
src/hooks/useRealtimePosts.tsSubscribes to postgres_changes on posts filtered by category_id
src/hooks/useVote.tsINSERT/DELETE votes with optimistic update and rollback
src/pages/Home.tsxFull feed of all posts
src/pages/Category.tsxCategory-filtered post feed
src/pages/PostDetail.tsxFull post + threaded comment section
src/pages/NewPost.tsxCreate post form page
src/pages/UserProfile.tsxUser profile with post history and karma
src/pages/Search.tsxFull-text search over posts
src/pages/ModerationDashboard.tsxOpen reports queue with moderator actions
supabase/functions/moderate-post/index.tsOpenAI Moderation API call returning flagged status
supabase/functions/notify-mention/index.tsParses @mentions and sends Resend notification emails
Routes
Full post feed sorted by recency
Category-filtered feed
Post detail with threaded comment section
Create new post (requires auth)
User profile with post history and karma
Full-text search results
Moderation dashboard with reports queue
Sign in page
Registration page
DB Tables
profiles| Column | Type |
|---|---|
| id | uuid primary key references auth.users.id |
| username | text unique not null |
| karma | int default 0 |
| role | text default 'member' |
RLS: Public-read; own write; role enforced via JWT app_metadata, not this column
posts| Column | Type |
|---|---|
| id | uuid primary key |
| author_id | uuid not null references auth.users.id |
| category_id | uuid not null references categories(id) |
| is_deleted | bool default false |
| vote_score | int default 0 (denormalized) |
RLS: Public-read WHERE NOT is_deleted; INSERT WITH CHECK calls posts_rate_limit()
comments| Column | Type |
|---|---|
| id | uuid primary key |
| post_id | uuid not null references posts(id) on delete cascade |
| parent_comment_id | uuid references comments(id) |
| is_deleted | bool default false |
| vote_score | int default 0 |
RLS: Public-read WHERE NOT is_deleted; nested threading via parent_comment_id
votes| Column | Type |
|---|---|
| id | uuid primary key |
| voter_id | uuid not null references auth.users.id |
| target_type | text check in ('post','comment') |
| value | smallint check in (-1, 1) |
RLS: UNIQUE(voter_id, target_type, target_id); AFTER INSERT/DELETE trigger updates vote_score on parent
reports| Column | Type |
|---|---|
| id | uuid primary key |
| target_type | text check in ('post','comment','user') |
| status | text default 'open' |
RLS: Reporter writes own; moderator reads and updates all
bans| Column | Type |
|---|---|
| id | uuid primary key |
| user_id | uuid references auth.users.id |
| expires_at | timestamptz |
RLS: Moderator-only read/write
categories| Column | Type |
|---|---|
| id | uuid primary key |
| slug | text unique not null |
| position | int default 0 |
RLS: Public-read; admin-write via has_role()
Components
PostCardCompact post row with vote arrows and metadata
CommentThreadRecursive nested comment renderer with collapse
VoteArrowsOptimistic upvote/downvote with INSERT into votes
ModeratorGateJWT role check for moderator and admin routes
NewPostFormCreate post with rate-limit exception handling
Follow-up prompts
Paste these into Agent Mode one by one, in order, after the starter prompt finishes.
Add OpenAI Moderation on post creation
Automatic content moderation on every post using OpenAI's Moderation API — flagged posts are queued for review rather than published
Wire the moderate-post Edge Function into the post creation flow.
In NewPostForm, before calling supabase.from('posts').insert(), call the moderate-post Edge Function with the post title and body concatenated. If flagged=true:
- Still INSERT the post row, but with is_deleted=true (hides from public feed)
- Immediately INSERT a reports row with target_type='post', reason='auto_moderation_flagged', reporter_id=null (system)
- Show the author a toast: 'Your post is being reviewed by a moderator before it appears publicly.'
If flagged=false, INSERT normally with is_deleted=false.
In ModerationDashboard, show auto-flagged posts in a separate 'AI Flagged' tab alongside user-reported content. Each row shows the flagged categories (e.g., 'hate, violence') and moderator actions: Approve (set is_deleted=false, close report), Delete (keep is_deleted=true), Ban Author.
Make sure OPENAI_API_KEY is in Cloud tab → Secrets before testing.When to use: Immediately after the starter prompt — before opening the forum to public signups
Wire Realtime feed updates
New posts appear live in the feed without page reload, with a smooth fade-in animation
Add live feed updates using Supabase Realtime.
In useRealtimePosts, create a Realtime subscription to postgres_changes on the posts table:
supabase.channel('posts:' + (categorySlug || 'all'))
.on('postgres_changes', {
event: 'INSERT',
schema: 'public',
table: 'posts',
filter: categoryId ? 'category_id=eq.' + categoryId : undefined
}, (payload) => {
queryClient.setQueryData(['posts', categorySlug], (old) =>
[payload.new, ...old]
);
})
.subscribe();
Return the cleanup function to unsubscribe on unmount.
In Home.tsx and Category.tsx, use useRealtimePosts to get the live subscription. New posts should fade into the top of the feed using a Framer Motion animate-in or a simple CSS transition.
Confirm in the migration that ALTER PUBLICATION supabase_realtime ADD TABLE posts; ran — without it, the subscription gets no events.When to use: Once the basic post feed works correctly
Add @mention parsing and Resend notifications
@mention detection in comments with Resend email notifications, respecting per-user email preference
Wire mention detection and email notifications.
Add a Postgres trigger on comments INSERT that fires after the comment is saved. The trigger should call pg_net.http_post to the notify-mention Edge Function URL with a JSON body containing: {post_id, comment_id, body: NEW.body, author_id: NEW.author_id}.
In notify-mention/index.ts:
- Parse body for @username patterns: const mentions = body.match(/@([a-zA-Z0-9_]+)/g)
- For each match, look up the profile by username
- Skip if mentioned user is the commenter themselves
- Check profile has an email in auth.users
- Send a Resend email with subject: '@{commenter_username} mentioned you in {post_title}' and body with a link to /post/${post_id}#comment-${comment_id}
- Respect a notify_email boolean in profiles (add this column: notify_email bool default true)
In UserProfile page, add a Notifications Settings section with a toggle for email notifications that updates profiles.notify_email.When to use: Once people start having active conversations and need @mention alerts
Build moderator action toolbar
Per-post moderator toolbar with delete, lock, pin, and ban actions plus an audit log of moderator actions
Add a floating moderator toolbar on every post and comment visible only to moderators. Create a ModeratorActions component that wraps each PostCard and CommentThread row. When the current user's JWT app_metadata.role is 'moderator' or 'admin', render a small icon row on hover: Delete (trash icon), Lock (lock icon, posts only), Pin (pin icon, posts only), Ban Author (shield icon). Actions: - Delete: set is_deleted=true, close any reports for this target - Lock: set is_locked=true on the post; locked posts show 'Locked by moderator' banner and disable new comments - Pin: set is_pinned=true; pinned posts always appear at top of category feed - Ban: open a BanDialog with reason text + expires_in_days selector; INSERT into bans table; set app_metadata.role='banned' via Edge Function (calls Supabase Admin API with service role key) Banned users see read-only experience — feed and posts visible but all write actions show 'Your account is suspended' toast. Create an audit_log table (id, actor_id, action, target_type, target_id, created_at) and write one row per moderator action.
When to use: Once the first spam wave hits — which will happen within days of public launch
Add karma display and feed sort modes
Karma accumulation from upvotes, per-user karma display, and New/Hot/Top feed sort modes with Hacker News-style decay
Add karma accumulation and multiple feed sort modes.
Karma update: add a Postgres trigger on votes INSERT that updates profiles.karma += NEW.value for the author of the voted post or comment. On votes DELETE, subtract OLD.value. Only count upvotes from other users (not self-votes — add a check: IF NEW.voter_id != author_id).
Display karma in PostCard author byline ('u/username · 1.2k karma') and on UserProfile page.
Add sort mode selector to Home.tsx and Category.tsx: tabs or dropdown for New / Hot / Top.
- New: ORDER BY created_at DESC
- Top: ORDER BY vote_score DESC
- Hot (Hacker News decay): ORDER BY vote_score / POWER(EXTRACT(EPOCH FROM now() - created_at) / 3600 + 2, 1.5) DESC — compute in a Postgres view
Create a v_posts_hot view with the decay formula. Home.tsx fetches from this view for Hot mode.
Persist the selected sort mode in localStorage so the user's preference is remembered across sessions.When to use: Once the forum has 50+ posts and needs discovery beyond chronological order
Add post image attachments via Storage
Optional image attachments on posts with 5MB limit and inline preview in feed and post detail
Add optional image upload to posts.
Create a 'post-attachments' Storage bucket with Public ON. Set upload policy: bucket_id='post-attachments' AND auth.uid() IS NOT NULL AND (octet_length(decode(split_part(name, '.', -1), 'hex')) < 5242880). In practice, enforce the 5MB limit in the client before upload.
Add attachments jsonb[] column to posts table (array of {storage_path, content_type, size_bytes}).
In NewPostForm, add an optional image picker below the body textarea. On file select, validate type (jpg/png/webp) and size (<5MB). Upload to post-attachments/${author_id}/${uuid}-${filename} before inserting the post row. Store the returned path in the attachments array.
In PostCard and PostDetail, if posts.attachments is non-empty and first attachment is image type, render a rounded image below the title (max-height 400px, object-cover). In PostCard (list view), render as a small 64x64 thumbnail on the right side.When to use: When text-only posts feel limiting for showcase or tutorial categories
Add user reputation gating for new accounts
New-account post restrictions with automatic moderation queue, reducing coordinated bot spam without CAPTCHA
Add new-account restrictions to reduce drive-by spam. Add two columns to profiles: post_count int default 0 (updated by trigger on posts INSERT for non-deleted posts), account_age_days computed as EXTRACT(DAY FROM now() - created_at). Define 'new user' as: account_age_days < 7 OR post_count < 3. Restrictions for new users: - Can only post in the 'General' category (other category post buttons are disabled with tooltip 'Post in General first to unlock all categories') - Limited to 1 post per day instead of 5 per 10 minutes - Posts from new users are automatically queued for moderation (set is_deleted=true, create reports row with reason='new_user_review') regardless of OpenAI Moderation result Show a non-dismissible banner to new users: 'Your first 3 posts are reviewed before appearing publicly. This lifts automatically as you build reputation.' Once post_count >= 3 and account_age_days >= 7, normal rate limits and direct publication apply.
When to use: Once you see moderator workload from throwaway accounts — after the OpenAI Moderation follow-up is already in place
Common errors
Real error strings you'll see. Find yours, paste the fix prompt.
P0001: Rate limit exceeded: max 5 posts per 10 minutesThe posts_rate_limit() BEFORE INSERT trigger fired correctly — the user (or a bot) exceeded 5 posts in 10 minutes. This is the system working, but your UI is showing a generic 500 error instead of a friendly message.
In NewPostForm, catch the Supabase error and check error.code === 'P0001' or error.message.includes('Rate limit'). Show a shadcn toast: 'Too many posts. You can post again in a few minutes.' Store last_post_at in localStorage on successful post to compute a countdown client-side. Do NOT lower the rate limit — it's protecting you from bots.Vote arrows update visually but reload restores old countYour useVote hook updated local TanStack Query cache (optimistic update) but the AFTER INSERT trigger on votes didn't fire or wasn't created — so the posts.vote_score column never updated in the database.
Check that the votes_score_trigger exists: SELECT * FROM information_schema.triggers WHERE trigger_name = 'votes_score_trigger'. If missing, run: CREATE TRIGGER votes_score_trigger AFTER INSERT OR DELETE ON votes FOR EACH ROW EXECUTE FUNCTION update_target_vote_score(). Also confirm update_target_vote_score() function exists. After fixing, test: insert a vote via SQL and verify posts.vote_score changed.
Posts appear in Realtime feed for category A even when viewing category BYour useRealtimePosts subscription has no filter set, or the filter string is malformed — every subscriber receives every INSERT on the posts table.
In useRealtimePosts, confirm the filter is set: .on('postgres_changes', {event: 'INSERT', schema: 'public', table: 'posts', filter: 'category_id=eq.' + categoryId}, handler). Each category page needs its own Realtime channel name: supabase.channel('posts:' + categoryId). If categoryId is undefined on the home page, omit the filter entirely and subscribe to all inserts. Clean up: confirm ALTER PUBLICATION supabase_realtime ADD TABLE posts ran in the migration.Moderator deletes a post but it still appears in the feed after refreshYour posts SELECT RLS policy doesn't filter on NOT is_deleted, so all posts (including soft-deleted ones) are returned to the client and the soft-delete is just a client-side filter.
Update the posts SELECT RLS policy: DROP POLICY IF EXISTS public_read_posts ON posts; CREATE POLICY public_read_posts ON posts FOR SELECT USING (NOT is_deleted). After this, deleted posts return zero rows to any client. Also invalidate the TanStack Query cache client-side after a moderator delete: queryClient.invalidateQueries({queryKey: ['posts']}).Spam wave creates 200 posts/minute — rate limit not stopping itBots are creating fresh accounts for each post, bypassing the per-user 5-per-10-minute rate limit. Your signup flow has no CAPTCHA or email verification.
Two-layer fix: (1) Enable email confirmation in Cloud tab → Users & Auth → Email auth → 'Confirm email' = ON. This forces bot-signup to require email access. (2) Add a global rate-limit trigger as a second defense: CREATE FUNCTION posts_global_rate_limit() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF (SELECT COUNT(*) FROM posts WHERE created_at > now() - interval '1 minute') > 30 THEN RAISE EXCEPTION 'Global rate limit'; END IF; RETURN NEW; END; $$; CREATE TRIGGER posts_global_limit BEFORE INSERT ON posts FOR EACH ROW EXECUTE FUNCTION posts_global_rate_limit().
infinite recursion detected in policy for relation "posts"Your posts SELECT RLS policy references the votes or comments table to compute vote_score or comment visibility, and one of those policies in turn references posts — creating a circular dependency.
Drop all circular cross-table references from your RLS policies. Make the posts SELECT policy self-contained: CREATE POLICY public_read_posts ON posts FOR SELECT USING (NOT is_deleted). Move vote counting to the denormalized vote_score column updated by the votes_score_trigger. Move comment count to a denormalized comment_count column updated by a AFTER INSERT/DELETE trigger on comments. Never compute aggregates inside RLS policies.
Cost reality
What this build actually costs — no surprises on your card.
Recommended Lovable plan
Pro $25/mo. The Realtime subscription debugging and rate-limit trigger iteration are where credits go. Free plan's ~30 monthly cap won't cover the full chain.
Monthly run cost breakdown
~100-180 credits (starter ~60-80, follow-ups 1-4 ~190 if all done, follow-ups 5-7 add ~120 more) total| Item | Cost |
|---|---|
| Lovable Pro Drop to Free after build — the forum runs on Cloud | $25/mo |
| Supabase / Lovable Cloud 500MB DB handles ~5M posts/comments; 200 concurrent Realtime connections covers ~100 live viewers | $0/mo at MVP |
| OpenAI Moderation API The Moderation endpoint is effectively bundled with OpenAI accounts — rarely billed separately at forum scale | ~$0/mo |
| Resend Covers mention notifications for first ~1K active members | $0/mo up to 3K emails |
| Custom domain Via Cloud tab → Publish → custom domain on Pro plan | $10-15/yr |
Scaling notes: The most likely first ceiling is Supabase Realtime concurrent connections (200 Free, 500 Pro). Once you have ~100 daily active users with tabs open simultaneously, you'll hit the Free cap during peak. Supabase Pro at $25/mo buys you to 500 concurrent. Past that, consider rate-limited polling with stale-while-revalidate for the feed instead of per-client Realtime subscriptions. The 500MB DB cap handles ~5M small posts before you need 8GB Pro.
Production checklist
Steps to take before you share the URL with real users.
Anti-Spam (before public launch)
Enable email confirmation
Cloud tab → Users & Auth → Email auth section → toggle 'Confirm email' ON. Unconfirmed accounts cannot post.
Verify posts_rate_limit trigger is active
SQL Editor: SELECT trigger_name, event_manipulation FROM information_schema.triggers WHERE event_object_table = 'posts'. Confirm posts_rate_limit_trigger appears with INSERT.
Test OpenAI moderation with a flagged post
Submit a post containing obvious policy-violating text and confirm it gets routed to /mod reports queue with auto_moderation_flagged reason rather than appearing in the feed.
Domain & SSL
Connect custom domain
Click Publish (top-right) → Settings → Custom domain. Add CNAME record pointing to your Lovable subdomain. SSL auto-provisions.
Update OAuth redirect URLs if using Google
Cloud tab → Users & Auth → Google provider → update Redirect URL to https://yourdomain.com/auth/callback
Realtime Health
Verify Realtime publication includes posts and comments
SQL Editor: SELECT schemaname, tablename FROM pg_publication_tables WHERE pubname='supabase_realtime'. Confirm both posts and comments appear. If missing, run: ALTER PUBLICATION supabase_realtime ADD TABLE posts; ALTER PUBLICATION supabase_realtime ADD TABLE comments;
Set up vote-score drift correction cron
SQL Editor: SELECT cron.schedule('vote_score_repair', '0 3 * * 0', $$UPDATE posts p SET vote_score = (SELECT COALESCE(SUM(value),0) FROM votes WHERE target_type='post' AND target_id=p.id)$$). This runs weekly and self-heals any trigger-drift.
Monitoring
Check Cloud tab → Logs weekly
Cloud tab → Logs → Edge Functions. Look for failed moderate-post calls (OpenAI API errors) and failed notify-mention calls. Failed moderation means posts go live unreviewed.
Frequently asked questions
How do I stop spam without manual moderation around the clock?
Three layers working together: (1) Email confirmation on signup (Cloud tab → Users & Auth → Confirm email ON) — prevents disposable-email bot signups. (2) The posts_rate_limit() trigger rejects more than 5 posts per user per 10 minutes at the database level, before any client code runs. (3) The moderate-post Edge Function routes flagged content to the moderation queue automatically. With all three active, most spam waves burn out before causing visible damage. The new-account gating follow-up adds a fourth layer: posts from accounts younger than 7 days are auto-queued regardless of moderation result.
Does the OpenAI Moderation API cost money?
In practice, no. The Moderation endpoint (api.openai.com/v1/moderations) was originally free and is now bundled with any OpenAI API account. At forum scale (hundreds or thousands of posts per month), you will not see it on your OpenAI bill. It's a different endpoint from chat completions — it's specifically designed as a safety check and is priced near zero. Just add your OPENAI_API_KEY to Cloud tab → Secrets and it will work.
Can I handle 1,000 concurrent users on the Free tier?
Not comfortably, if they all have the Realtime feed open. Supabase Free caps at 200 concurrent Realtime connections. With 1,000 concurrent users and each browser tab holding one Realtime channel subscription, you'll see 403 errors and dropped connections starting around 200 users. The fix is Supabase Pro ($25/mo, 500 concurrent connections) or replacing the per-client Realtime subscription with a polling approach for the feed (fetch every 30 seconds with stale-while-revalidate). The 500MB DB cap itself handles 5M+ posts, so posts volume is not the constraint.
How do I prevent users from creating throwaway accounts to spam?
Enable email confirmation (Cloud tab → Users & Auth → Confirm email ON). This forces every new account to click a link in a real email before they can post — creating hundreds of throwaway accounts becomes time-consuming. For additional protection, add the new-account gating follow-up that auto-queues first posts from accounts younger than 7 days. If you're seeing bot waves even with email confirmation, add Cloudflare Turnstile to the signup form (free tier, ~1M solves per month) — the follow-up for spam wave handling includes this.
Should I use threaded comments or flat comments?
Threaded works best for Q&A and technical discussion categories where replies are directed at specific comments. Flat works better for casual conversation and shorter threads. This kit scaffolds threaded comments via parent_comment_id, rendered recursively up to 3 levels deep. Beyond 3 levels, show a 'continue this thread' link that opens the sub-thread in its own view. If you decide flat is better for your community, update CommentThread to ignore parent_comment_id and sort all comments by created_at instead.
How do I migrate an existing Discord community to this forum?
There is no automated migration path from Discord to a Lovable forum. What you can do: export Discord message history via the Data Export tab in Discord Server Settings, which gives you a ZIP of JSON files per channel. Write a one-time import script (a Supabase Edge Function is easiest) that reads the JSON, creates profiles for each Discord user, maps Discord channels to forum categories, and inserts posts and comments with created_at set to the original Discord timestamp. Votes cannot be migrated — start fresh. Plan for a transition period where you point people to the new forum while keeping Discord read-only for a month.
When does Discourse make more sense than building in Lovable?
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.