Skip to main content
RapidDev - Software Development Agency
Lovable PromptsSocialIntermediate

Build a Community Forum in Lovable

A threaded community forum with categories, posts and nested comments, upvote/downvote scoring via AFTER INSERT triggers, per-user rate-limiting via BEFORE INSERT trigger, OpenAI Moderation on post creation, Supabase Realtime live feed, and a moderator action queue.

Time to MVP

~1 day

Credits

~100-180 credits for full chain

Difficulty

Intermediate

Cloud features

4

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.

  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 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.

  1. 1Cloud tab → Users & Auth
  2. 2Under Sign-in methods, enable Email (already on by default); optionally enable Google
  3. 3Leave 'Allow new users to sign up' ON so members can self-register
  4. 4Enable email confirmation: scroll to 'Email auth' → 'Confirm email' → toggle ON. This is your first line of spam defense.
  5. 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.

  1. 1No manual setup needed before running the starter prompt
  2. 2After the starter finishes, confirm moderate-post and notify-mention appear in Cloud tab → Edge Functions
  3. 3Add OPENAI_API_KEY and RESEND_API_KEY in Cloud tab → Secrets before testing moderation

Secrets (Cloud tab → Secrets)

OPENAI_API_KEY

Purpose: 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_KEY

Purpose: 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.

lovable-agent-mode.txt
~60-80 credits
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.sql

7 tables + RLS + rate-limit trigger + vote-score trigger + has_role() + Realtime publication + seed categories

src/layouts/PublicLayout.tsx

Top nav with category pills, search, and user dropdown

src/components/AuthGuard.tsx

Redirects unauthenticated users to /login

src/components/ModeratorGate.tsx

Checks JWT app_metadata.role IN (moderator, admin)

src/components/PostCard.tsx

Compact post row with vote arrows, title, metadata

src/components/CommentThread.tsx

Recursive nested comment renderer with collapse/reply

src/components/VoteArrows.tsx

Optimistic upvote/downvote handler with user vote state

src/components/ReportDialog.tsx

Report reason form that creates reports rows

src/components/NewPostForm.tsx

Create post form with rate-limit error handling

src/hooks/useRealtimePosts.ts

Subscribes to postgres_changes on posts filtered by category_id

src/hooks/useVote.ts

INSERT/DELETE votes with optimistic update and rollback

src/pages/Home.tsx

Full feed of all posts

src/pages/Category.tsx

Category-filtered post feed

src/pages/PostDetail.tsx

Full post + threaded comment section

src/pages/NewPost.tsx

Create post form page

src/pages/UserProfile.tsx

User profile with post history and karma

src/pages/Search.tsx

Full-text search over posts

src/pages/ModerationDashboard.tsx

Open reports queue with moderator actions

supabase/functions/moderate-post/index.ts

OpenAI Moderation API call returning flagged status

supabase/functions/notify-mention/index.ts

Parses @mentions and sends Resend notification emails

Routes
/

Full post feed sorted by recency

/c/:categorySlug

Category-filtered feed

/post/:postId

Post detail with threaded comment section

/post/new

Create new post (requires auth)

/u/:username

User profile with post history and karma

/search

Full-text search results

/mod

Moderation dashboard with reports queue

/login

Sign in page

/signup

Registration page

DB Tables
profiles
ColumnType
iduuid primary key references auth.users.id
usernametext unique not null
karmaint default 0
roletext default 'member'

RLS: Public-read; own write; role enforced via JWT app_metadata, not this column

posts
ColumnType
iduuid primary key
author_iduuid not null references auth.users.id
category_iduuid not null references categories(id)
is_deletedbool default false
vote_scoreint default 0 (denormalized)

RLS: Public-read WHERE NOT is_deleted; INSERT WITH CHECK calls posts_rate_limit()

comments
ColumnType
iduuid primary key
post_iduuid not null references posts(id) on delete cascade
parent_comment_iduuid references comments(id)
is_deletedbool default false
vote_scoreint default 0

RLS: Public-read WHERE NOT is_deleted; nested threading via parent_comment_id

votes
ColumnType
iduuid primary key
voter_iduuid not null references auth.users.id
target_typetext check in ('post','comment')
valuesmallint check in (-1, 1)

RLS: UNIQUE(voter_id, target_type, target_id); AFTER INSERT/DELETE trigger updates vote_score on parent

reports
ColumnType
iduuid primary key
target_typetext check in ('post','comment','user')
statustext default 'open'

RLS: Reporter writes own; moderator reads and updates all

bans
ColumnType
iduuid primary key
user_iduuid references auth.users.id
expires_attimestamptz

RLS: Moderator-only read/write

categories
ColumnType
iduuid primary key
slugtext unique not null
positionint default 0

RLS: Public-read; admin-write via has_role()

Components
PostCard

Compact post row with vote arrows and metadata

CommentThread

Recursive nested comment renderer with collapse

VoteArrows

Optimistic upvote/downvote with INSERT into votes

ModeratorGate

JWT role check for moderator and admin routes

NewPostForm

Create post with rate-limit exception handling

Follow-up prompts

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

1

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

~50 credits
prompt
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

2

Wire Realtime feed updates

New posts appear live in the feed without page reload, with a smooth fade-in animation

~40 credits
prompt
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

3

Add @mention parsing and Resend notifications

@mention detection in comments with Resend email notifications, respecting per-user email preference

~50 credits
prompt
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

4

Build moderator action toolbar

Per-post moderator toolbar with delete, lock, pin, and ban actions plus an audit log of moderator actions

~50 credits
prompt
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

5

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

~40 credits
prompt
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

6

Add post image attachments via Storage

Optional image attachments on posts with 5MB limit and inline preview in feed and post detail

~50 credits
prompt
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

7

Add user reputation gating for new accounts

New-account post restrictions with automatic moderation queue, reducing coordinated bot spam without CAPTCHA

~30 credits
prompt
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 minutes

The 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.

Fix — paste into Lovable Agent Mode
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 count

Your 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.

Fix — paste into Lovable Agent Mode
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 B

Your useRealtimePosts subscription has no filter set, or the filter string is malformed — every subscriber receives every INSERT on the posts table.

Fix — paste into Lovable Agent Mode
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 refresh

Your 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.

Fix — paste into Lovable Agent Mode
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 it

Bots 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.

Fix — paste into Lovable Agent Mode
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.

Fix — paste into Lovable Agent Mode
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
ItemCost
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 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.