Skip to main content
RapidDev - Software Development Agency
App Featurescommunication-social21 min read

How to Add a Community Forum to Your App (Copy-Paste Prompts Included)

A community forum needs a forum_posts table with parent_post_id for threading, a votes table with a signed integer and a unique constraint per user, full-text search via Supabase to_tsvector with a GIN index, and a moderator role check in RLS for pin/lock/delete actions. With Lovable or v0 you can ship a working threaded forum in 5-10 hours for $0/mo on free tiers, then $25/mo on Supabase Pro at 1,000 users.

4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members

Feature spec

Intermediate

Category

communication-social

Build with AI

5-10 hours with Lovable or v0

Custom build

3-6 weeks custom dev

Running cost

$0/mo under 100 users, $25/mo at 1,000 users

Works on

Web

Everything it takes to ship a Community Forum — parts, prompts, and real costs.

TL;DR

A community forum needs a forum_posts table with parent_post_id for threading, a votes table with a signed integer and a unique constraint per user, full-text search via Supabase to_tsvector with a GIN index, and a moderator role check in RLS for pin/lock/delete actions. With Lovable or v0 you can ship a working threaded forum in 5-10 hours for $0/mo on free tiers, then $25/mo on Supabase Pro at 1,000 users.

What a Community Forum Actually Requires

A community forum is the structured discussion layer in a SaaS product, developer tool, or online community — the place where users ask questions, share knowledge, and debate ideas in threaded conversations. Unlike a real-time chat room, a forum persists indefinitely, is indexed by search engines, and gives moderators tools to curate quality. The architecture involves categorised thread lists, nested replies with parent-child relationships stored in a single table via parent_post_id, an upvote/downvote system with a unique-per-user constraint, full-text search using PostgreSQL's to_tsvector, and a role-based moderation system that gates pin, lock, and delete actions. Getting the recursive reply rendering and the moderator RLS right is where first builds fail — the flat rendering bug is the single most reported issue with AI-generated forums.

What users consider table stakes in 2026

  • Categorised topic listing with post count and a last-activity timestamp so users can see which areas are active
  • Threaded replies with at least 2 levels of nesting so users can reply to replies, not just the root post
  • Upvote and downvote system on posts and replies with an instantly visible score change
  • Full-text search across all threads and replies — users expect to find what they are looking for without browsing every category
  • User badges and post count shown next to the username to signal community standing
  • Moderator controls — pin to top, lock to prevent new replies, delete — visible only to users with the moderator role
  • Email notification when someone replies to a thread the user created or participated in

Anatomy of the Feature

Seven components build the forum. AI tools generate the category list and thread view correctly on first prompt — recursive nested replies, the moderator role check in RLS, and the full-text search GIN index are where builds fail.

Layers:UIDataBackendService

Category and channel list

UI

A shadcn/ui Accordion or sidebar navigation listing all forum categories. Each category row shows its name, a post count badge, and a preview of the most recent post title. Categories are stored in a forum_categories table sorted by sort_order. Clicking a category navigates to the thread list for that category.

Note: Keep the category list short — 5-12 categories is the right range for a product forum. Too many categories fragment discussions and make the forum feel empty.

Thread list

UI

A shadcn/ui DataTable or custom list showing threads in the selected category, sorted by last-activity DESC (the created_at of the most recent reply). Pinned threads appear at the top regardless of activity timestamp. Each row shows title, author avatar, reply count, score, and last-activity relative timestamp. Paginated via keyset cursor on last-activity timestamp.

Note: Sort by last-activity, not created_at. A thread with 50 replies from today should appear above a new thread with 0 replies from an hour ago.

Thread detail and replies

UI

A recursive React component that renders nested replies. The root post renders at the top. Direct replies (parent_post_id = root post id) render at level 1. Replies to replies render at level 2. Depth is capped at 3 levels to prevent infinite nesting. Each node contains: author avatar, name, post count badge, relative timestamp, body text, score, and a Reply button that opens an inline reply composer.

Note: Cap render depth at 3 levels. Beyond that, show a 'Continue thread' link that navigates to the parent post context. Deeper nesting creates visual complexity that most users cannot parse.

Voting system

Data

A forum_votes table with a value column constrained to 1 or -1. A UNIQUE(post_id, user_id) constraint prevents multiple votes. Score is computed as SUM(value) in the posts query. On vote: INSERT with ON CONFLICT (post_id, user_id) DO UPDATE SET value = EXCLUDED.value — this handles vote flipping (upvote to downvote) without a separate DELETE+INSERT.

Note: Compute score via SUM(value) at query time or maintain a denormalised score column updated by a trigger. For forums under 100,000 posts, the SUM(value) JOIN approach is fast enough. Denormalisation is only worth the complexity at scale.

Thread search

Backend

Supabase full-text search using to_tsvector('english', title || ' ' || body) on the forum_posts table. A GIN index on the tsvector makes search fast. The search query uses to_tsquery to match the user's input. Results are sorted by ts_rank() for relevance. Algolia InstantSearch is the upgrade path for typo-tolerant search at scale.

Note: The GIN index is not created automatically — it must be explicitly added. Without it, to_tsvector search does a sequential scan on every query, which becomes unusable past a few thousand posts.

Moderation actions

Backend

Pin, lock, and delete actions gated by a moderator role stored in the user's profiles table. RLS checks auth.jwt() ->> 'role' = 'moderator' for UPDATE (pin/lock) and DELETE operations on forum_posts. Locked threads show a 'This thread is locked' banner and the reply form is disabled client-side. Soft-delete sets deleted_at rather than hard-deleting rows.

Note: Moderator role must be enforced in RLS — client-side hiding of the moderation buttons is not security. A user who inspects network requests can send DELETE directly to the Supabase API if RLS is absent.

Reply notification

Service

A Supabase Edge Function triggered by INSERT on forum_posts (where parent_post_id IS NOT NULL) looks up the root thread author and sends an email via the Resend API with a link back to the thread. Includes a guard: do not send if the commenter is the thread author (self-reply notification).

Note: Add a guard in the Edge Function: if (newReply.author_id === threadPost.author_id) return early. Without this guard, every time an author replies to their own thread they receive a self-notification email.

The data model

Three tables form the forum schema: categories, posts (with self-referential parent for threading), and votes. Run this in the Supabase SQL editor.

schema.sql
1create table public.forum_categories (
2 id uuid primary key default gen_random_uuid(),
3 name text not null,
4 slug text not null unique,
5 description text,
6 sort_order int not null default 0,
7 created_at timestamptz not null default now()
8);
9
10create table public.forum_posts (
11 id uuid primary key default gen_random_uuid(),
12 category_id uuid references public.forum_categories(id) on delete set null,
13 author_id uuid references auth.users(id) on delete set null not null,
14 parent_post_id uuid references public.forum_posts(id) on delete cascade,
15 title text,
16 body text not null,
17 is_pinned bool not null default false,
18 is_locked bool not null default false,
19 deleted_at timestamptz,
20 created_at timestamptz not null default now(),
21 updated_at timestamptz not null default now(),
22 -- title is required only for root posts (parent_post_id is null)
23 check (parent_post_id is not null or title is not null)
24);
25
26create table public.forum_votes (
27 post_id uuid references public.forum_posts(id) on delete cascade not null,
28 user_id uuid references auth.users(id) on delete cascade not null,
29 value smallint not null check (value in (1, -1)),
30 created_at timestamptz not null default now(),
31 primary key (post_id, user_id)
32);
33
34alter table public.forum_categories enable row level security;
35alter table public.forum_posts enable row level security;
36alter table public.forum_votes enable row level security;
37
38-- Categories are public
39create policy "categories are public"
40 on public.forum_categories for select
41 using (true);
42
43-- Posts are publicly readable (exclude deleted)
44create policy "posts are public"
45 on public.forum_posts for select
46 using (deleted_at is null);
47
48-- Authenticated users can create posts and replies
49create policy "authenticated can post"
50 on public.forum_posts for insert
51 to authenticated
52 with check (auth.uid() = author_id);
53
54-- Authors can edit their own posts
55create policy "author can update own"
56 on public.forum_posts for update
57 using (auth.uid() = author_id)
58 with check (auth.uid() = author_id);
59
60-- Moderators can pin, lock, or soft-delete any post
61create policy "moderator can update any"
62 on public.forum_posts for update
63 using (auth.jwt() ->> 'role' = 'moderator');
64
65create policy "moderator delete"
66 on public.forum_posts for delete
67 using (
68 auth.jwt() ->> 'role' = 'moderator'
69 or auth.uid() = author_id
70 );
71
72-- Votes are public
73create policy "votes are public"
74 on public.forum_votes for select
75 using (true);
76
77-- Authenticated users can vote
78create policy "authenticated can vote"
79 on public.forum_votes for insert
80 to authenticated
81 with check (auth.uid() = user_id);
82
83-- Users can change their vote
84create policy "authenticated can update vote"
85 on public.forum_votes for update
86 using (auth.uid() = user_id);
87
88create policy "authenticated can delete vote"
89 on public.forum_votes for delete
90 using (auth.uid() = user_id);
91
92-- Full-text search index (critical for search performance)
93create index forum_posts_fts
94 on public.forum_posts
95 using gin (to_tsvector('english', coalesce(title, '') || ' ' || body))
96 where deleted_at is null;
97
98-- Thread list performance indexes
99create index forum_posts_category_created_idx
100 on public.forum_posts (category_id, created_at desc)
101 where parent_post_id is null and deleted_at is null;
102
103create index forum_posts_parent_idx
104 on public.forum_posts (parent_post_id, created_at asc)
105 where deleted_at is null;
106
107create index forum_votes_post_idx
108 on public.forum_votes (post_id);

Heads up: The GIN index on to_tsvector is partial (WHERE deleted_at IS NULL) so deleted posts are excluded from search results automatically. The parent_idx on (parent_post_id, created_at ASC) makes loading replies for a thread fast as reply counts grow. The CHECK constraint on forum_posts enforces that root posts must have a title while replies do not.

Build it — pick your path

Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.

Full-stack, non-technical friendlyFit for this feature:

Good for scaffolding the category list, thread view, and reply composer with Supabase auto-wired. Nested reply rendering and the moderator role check often need correction on first pass.

Step by step

  1. 1Create a new Lovable project and connect Lovable Cloud so Supabase is provisioned with auth
  2. 2Paste the prompt below in Agent Mode — Lovable generates the category sidebar, thread list, thread detail with nested replies, voting system, and search
  3. 3Add RESEND_API_KEY to the Secrets panel (Cloud tab) to enable reply notification emails
  4. 4Verify the recursive reply rendering: create a root post, reply to it, then reply to that reply — confirm 3 levels render correctly and the 4th level shows 'Continue thread'
  5. 5Test moderator actions by setting your user's role to 'moderator' via the Supabase Auth admin dashboard and verifying pin, lock, and delete buttons appear
Paste into Lovable
Build a community forum using Supabase. Create three tables: forum_categories (id uuid pk, name, slug unique, description, sort_order, created_at), forum_posts (id uuid pk, category_id references forum_categories, author_id references auth.users, parent_post_id references forum_posts nullable, title text nullable, body text not null, is_pinned bool default false, is_locked bool default false, deleted_at timestamptz, created_at, updated_at; CHECK that parent_post_id IS NOT NULL OR title IS NOT NULL), forum_votes (post_id, user_id, value smallint CHECK value IN (1,-1), PRIMARY KEY (post_id, user_id)). Enable RLS: posts and categories SELECT is public; authenticated INSERT where author_id = auth.uid(); moderators (JWT role = 'moderator') can UPDATE is_pinned/is_locked and DELETE any post; authors can soft-delete own posts. UI: Category sidebar with post count per category. Thread list for selected category: sorted by created_at DESC with pinned threads always first; keyset pagination 20 threads per page; show score (SUM of forum_votes.value). Thread detail: recursive React component rendering nested replies from parent_post_id — cap depth at 3 levels, show 'Continue thread' link at level 4. Reply composer inline below each post (disabled if is_locked = true; show 'Thread is locked' banner). Voting: upvote/downvote buttons; INSERT on forum_votes with ON CONFLICT (post_id, user_id) DO UPDATE SET value = EXCLUDED.value to handle vote flipping. Moderator controls (visible when JWT role = 'moderator'): pin button, lock button, delete button. Full-text search input using Supabase to_tsvector('english', title || ' ' || body) with GIN index. Add GIN index: CREATE INDEX forum_posts_fts ON forum_posts USING GIN(to_tsvector('english', coalesce(title,'') || ' ' || body)) WHERE deleted_at IS NULL. Reply notification: Supabase Edge Function on posts INSERT where parent_post_id IS NOT NULL — look up root thread author, send Resend email with link to thread; skip if commenter = thread author. Empty state for new categories. Soft-deleted posts show 'Post removed' placeholder.

Where this path bites

  • Recursive nested reply rendering is frequently generated flat (all replies at the same level) on first pass — verify by creating a reply-to-a-reply and checking the indented rendering
  • Moderator role check is sometimes added to the frontend only, not to RLS — verify the DELETE and UPDATE policies exist in the Supabase dashboard
  • Full-text search GIN index must be explicitly requested; Lovable does not add it automatically

Third-party services you'll need

The forum runs almost entirely on Supabase. Email notifications add Resend, and advanced search optionally adds Algolia.

ServiceWhat it doesFree tierPaid from
SupabasePostgreSQL for posts/votes/categories, full-text search via to_tsvector, Edge Functions for reply notificationsFree: 500 MB DB sufficient for tens of thousands of postsPro $25/mo for production uptime and connection headroom
ResendReply notification emails sent to thread authorsFree: 3,000 emails/mo — sufficient for small communitiesPro $20/mo: 50,000 emails (approx)
AlgoliaTypo-tolerant full-text search as an upgrade from Supabase to_tsvectorFree: 10,000 records, 10,000 search operations/moPay-as-you-go $0.50/1K operations after free tier (approx)

Swipe the table sideways to see pricing.

What it costs to run

Drag through the tiers to see how your monthly bill scales with users — no surprises later.

Estimated monthly running cost

$0/mo

Supabase free tier handles the forum volume. Resend free tier covers reply notifications. Algolia free tier covers search if used. Zero cost for a small community.

Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.

What breaks when AI tools build this

The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.

Nested replies render flat — all at the same indentation

Symptom: AI stores replies in the forum_posts table with a parent_post_id column but renders all posts at the same level in JSX by iterating the full flat array. The thread looks like a flat comment list regardless of the nesting structure in the database. The parent_post_id is in the data but ignored in the render.

Fix: Build a recursive React component that accepts a post and filters its children: const children = allPosts.filter(p => p.parent_post_id === post.id). Render each child recursively with increased left margin (e.g., pl-6 per level). Cap at 3 levels and show a 'Continue thread' link at level 4 to prevent infinite nesting.

Users can vote multiple times on the same post

Symptom: AI generates an INSERT on the forum_votes table without a unique constraint on (post_id, user_id). A user who clicks upvote 5 times inserts 5 rows with value = 1, inflating the score by 5. The score compounds with each page refresh.

Fix: Add PRIMARY KEY (post_id, user_id) on the forum_votes table and use INSERT ... ON CONFLICT (post_id, user_id) DO UPDATE SET value = EXCLUDED.value. This prevents duplicate votes and handles vote flipping (changing from up to down) in a single upsert.

Moderator cannot delete posts — RLS blocks the server-side call

Symptom: The moderator button appears in the UI because the client-side check passes (the user's JWT shows role = 'moderator'), but when the DELETE request reaches Supabase, the RLS policy only allows the author to delete their own posts. The moderator's DELETE is blocked at the database level with a silent 0 rows affected response.

Fix: Add the moderator DELETE policy: CREATE POLICY "moderator delete" ON forum_posts FOR DELETE USING (auth.jwt() ->> 'role' = 'moderator' OR author_id = auth.uid()). Verify in the Supabase dashboard that this policy exists and is enabled. Test by logging in as a moderator and deleting someone else's post.

Full-text search returns nothing or is very slow

Symptom: The to_tsvector search query runs but returns no results or times out on large post tables. Usually two causes: the GIN index was not created (every search does a sequential full-table scan), or the tsvector expression in the query does not match the one in the index (e.g., coalesce(title,'') in the query but title alone in the index).

Fix: Create the GIN index explicitly: CREATE INDEX forum_posts_fts ON forum_posts USING GIN(to_tsvector('english', coalesce(title,'') || ' ' || body)) WHERE deleted_at IS NULL. Make sure the to_tsvector expression in the query matches the index expression exactly. Check the Supabase query plan in the SQL editor with EXPLAIN ANALYZE to confirm the index is being used.

Reply notification emails sent to the author of their own reply

Symptom: The Supabase Edge Function fires on every INSERT on forum_posts where parent_post_id is not null. It looks up the root thread author and sends an email. When the thread author themselves posts a reply in their own thread, they receive a notification about their own reply — confusing and spammy.

Fix: Add a guard at the start of the Edge Function: const newReply = payload.record; if (newReply.author_id === rootPost.author_id) return new Response('skipped: self reply', { status: 200 }). Always short-circuit before calling the Resend API when the commenter and the thread author are the same user.

Best practices

1

Always build the recursive ReplyTree component with parent_post_id filtering — generating a flat comment list with no threading is the most common first-pass failure

2

Add the UNIQUE(post_id, user_id) constraint on forum_votes and use an upsert for vote flipping — two separate INSERT and DELETE calls create race conditions

3

Enforce moderator actions in RLS, not just in the client — a moderator check that only hides buttons can be bypassed with direct API calls

4

Create the GIN index on to_tsvector explicitly after schema creation — Supabase does not add it automatically and search is unusable without it at scale

5

Sort the thread list by last-activity timestamp (the created_at of the most recent reply) not thread created_at — active threads should surface to the top

6

Add the self-reply guard to the notification Edge Function before launch — it will fire on the first test if omitted

7

Cap nested reply depth at 3 levels in the render component — deeper nesting is unreadable on mobile and encourages low-quality sub-thread rabbit holes

When You Need Custom Development

Lovable and v0 handle a product forum with categories, threads, voting, and search at small-to-medium scale. Four requirements push beyond what AI tools build reliably:

  • You need Algolia or Typesense for typo-tolerant search across hundreds of thousands of posts — Supabase to_tsvector is exact-match only and does not handle misspellings
  • You need multi-tier moderation with appeals, ban management, and audit logs — a pin/lock button is not a moderation system for a community at scale
  • You need SSO integration (Okta, SAML) so enterprise users log in with their company credentials without creating a new account
  • You need an AI-powered duplicate detection or pre-moderation system that classifies posts before they go live — Supabase INSERT triggers can call an LLM API, but the full review queue UI is a custom build

RapidDev builds these features for production

Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.

Get a fixed-price quote

Frequently asked questions

How do I build a community forum in Lovable?

One Lovable prompt covers the full forum: specify parent_post_id for nested threading, upvote/downvote with signed integer score, moderator role controls, full-text search with to_tsvector and a GIN index, and reply email notifications via Resend. The total build time is 5-8 hours. The one thing to verify manually is that the recursive reply component uses parent_post_id filtering — Lovable sometimes generates a flat list on the first pass.

Can I add threaded replies to a forum?

Yes — add a parent_post_id column (nullable, references the same posts table) to your forum_posts table. Root threads have parent_post_id = null; replies store the ID of their parent. Build a recursive React component that fetches all posts for a thread, then filters children where parent_post_id = parentPost.id at each render depth. Cap depth at 3 levels in the component to prevent infinite nesting.

How do I implement upvoting in a forum?

Create a forum_votes table with a composite PRIMARY KEY on (post_id, user_id) and a value column constrained to 1 or -1. Use INSERT ... ON CONFLICT (post_id, user_id) DO UPDATE SET value = EXCLUDED.value so a user can switch from upvote to downvote without a separate delete. Compute the post score with SELECT SUM(value) FROM forum_votes WHERE post_id = :id.

How do I add moderator controls to a forum?

Store a moderator role in the user's profile or as a custom JWT claim in Supabase Auth. Show pin, lock, and delete buttons only when the current user's role is 'moderator'. Critically, also add RLS policies that enforce the restriction server-side: CREATE POLICY "moderator can update" ON forum_posts FOR UPDATE USING (auth.jwt() ->> 'role' = 'moderator'). Client-side hiding alone can be bypassed.

What is the best way to add search to a community forum?

Supabase full-text search with to_tsvector is the right first choice — it is built into PostgreSQL, requires no extra service, and handles exact-word search well. Add a GIN index on to_tsvector('english', coalesce(title,'') || ' ' || body) with a WHERE deleted_at IS NULL filter. When your post count exceeds 100,000 or users need typo-tolerant search, migrate to Algolia or Typesense and sync posts via a webhook.

How do I notify users by email when someone replies to their post?

Create a Supabase Edge Function triggered by INSERT on forum_posts where parent_post_id IS NOT NULL. In the function, fetch the root thread to get the thread author's ID. Look up their email in auth.users using the service role key. Call the Resend API to send a notification email with a link back to the thread. Add a guard: skip the email if the commenter's author_id equals the thread author's ID to avoid self-notifications.

Can I build a forum without a separate forum platform like Discourse?

Yes — Discourse is a full standalone application with its own hosting requirements, authentication, and admin panel. Building your forum directly in Supabase means it is part of your existing app, shares your auth system, and does not require embedding an external iframe or managing a separate service. The trade-off is you build moderation tooling yourself, but Lovable can scaffold the basics in a single session.

How many users can a Supabase-backed forum handle?

Supabase Pro ($25/mo) comfortably supports a forum with 10,000 registered users and hundreds of thousands of posts. The critical optimisations are the GIN index for search and the composite indexes on (category_id, created_at) for thread lists. Past 100,000 posts and 50,000 active users, consider adding read replicas (Supabase Team plan) and moving search to Algolia.

RapidDev

Need this feature production-ready?

RapidDev builds a community forum into real apps — auth, database, payments — at $13K–$25K.

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.