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

How to Add User Groups to Your App (Copy-Paste Prompts Included)

User groups need four pieces: a groups table with slugs and avatars, a group_members join table with roles (owner/admin/member), RLS policies that scope all reads to members, and an invite system that sends tokenized email links. With Lovable or V0 you can ship the full feature in 4–8 hours. Running cost starts at $0/month — Supabase and Resend free tiers cover you to ~1,000 users.

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

Feature spec

Intermediate

Category

communication-social

Build with AI

4–8 hours with Lovable or V0

Custom build

1–2 weeks custom dev

Running cost

$0/mo up to ~1K users · $25–45/mo at scale

Works on

WebMobile

Everything it takes to ship User Groups — parts, prompts, and real costs.

TL;DR

User groups need four pieces: a groups table with slugs and avatars, a group_members join table with roles (owner/admin/member), RLS policies that scope all reads to members, and an invite system that sends tokenized email links. With Lovable or V0 you can ship the full feature in 4–8 hours. Running cost starts at $0/month — Supabase and Resend free tiers cover you to ~1,000 users.

What a User Groups Feature Actually Is

User groups let people cluster into named collections — teams, communities, cohorts, guilds — and then gate content, discussions, and permissions to those collections. The feature has three functional layers: the group itself (name, slug, avatar), membership with role hierarchy (owner, admin, member), and group-scoped content where RLS enforces that non-members see nothing. The product decisions that vary by app are the invite mechanism (email vs. username), role permissions per tier, and what happens when the last owner leaves — a guard most AI-generated builds omit.

What users consider table stakes in 2026

  • Create, rename, and delete groups with a custom name (3–50 characters) and uploadable avatar
  • Invite members by email or username, with a 24-hour expiring invite link sent to their inbox
  • Role hierarchy — owner, admin, member — with a dropdown to change member roles and an audit trail
  • Real-time online indicators in the member list (green dot) via Supabase Presence
  • Group-scoped content feed, file list, or chat visible only to members
  • Leave or archive a group at any time without data loss; archive hides the group but preserves all content

Anatomy of the Feature

Six components. The RLS policy layer and the last-owner guard are the two places AI-generated builds consistently fail — be explicit about both in your prompt.

Layers:UIDataBackendService

Group card list

UI

Renders a scrollable list of the user's groups using shadcn/ui Card and Avatar components with a member count badge. React Query caches the list and revalidates when a new group is created or the user joins one.

Note: Include an empty state with a 'Create your first group' CTA — first-time users see this before anything else.

Group creation modal

UI

A shadcn/ui Dialog containing a react-hook-form form validated with zod: name (3–50 chars), optional description, and avatar upload. On submit, a slug is auto-generated from the name (lowercase, hyphens) and checked for uniqueness against the groups table before insert.

Note: Slug uniqueness must be validated server-side — client-only validation allows race conditions when two users create 'Team Alpha' simultaneously.

Member management panel

UI

A shadcn/ui DataTable built on TanStack Table v8 listing all members with columns: avatar, username, role dropdown (owner/admin/member), and a remove button. Role changes and removals apply optimistic UI updates and revert on error.

Note: Role dropdown for the owner row should disable 'remove' until ownership is transferred to prevent accidental lock-out.

Invitation system

Backend

A Supabase Edge Function receives (group_id, email), inserts a group_invites row with a UUID token and an expires_at 24 hours from now, then calls the Resend API to send the invite email with a link containing the token. The /invite/:token route on the frontend validates the token, checks expiry, and inserts a group_members row.

Note: The invite Edge Function must run server-side to keep the Resend API key out of the browser. Set RESEND_API_KEY in Cloud tab → Secrets.

Group-scoped RLS policies

Data

Supabase Row Level Security restricts all SELECT, INSERT, UPDATE, and DELETE operations on group content to authenticated users who appear in the group_members table for that group. The core policy pattern: `EXISTS (SELECT 1 FROM group_members WHERE group_id = groups.id AND user_id = auth.uid())`.

Note: Every table that stores group-scoped content needs its own policy. AI tools frequently create the groups table without policies, leaving group data world-readable.

Realtime presence

Service

Supabase Realtime Presence channel keyed by group_id tracks which members are currently viewing the group. Each member broadcasts a presence payload with their user_id and display name; the member list renders a green dot next to online users.

Note: Always call channel.unsubscribe() in the useEffect cleanup — presence state leaks if the user navigates away without unsubscribing.

The data model

Three tables: groups, group_members (the join table with roles), and group_invites. Run this in the Supabase SQL Editor — it creates the schema, enforces constraints, and wires RLS policies in one pass.

schema.sql
1create table public.groups (
2 id uuid primary key default gen_random_uuid(),
3 name text not null check (char_length(name) between 3 and 50),
4 slug text not null unique,
5 avatar_url text,
6 created_by uuid references auth.users(id) on delete set null,
7 created_at timestamptz not null default now()
8);
9
10create type public.group_role as enum ('owner', 'admin', 'member');
11
12create table public.group_members (
13 group_id uuid references public.groups(id) on delete cascade not null,
14 user_id uuid references auth.users(id) on delete cascade not null,
15 role public.group_role not null default 'member',
16 joined_at timestamptz not null default now(),
17 primary key (group_id, user_id)
18);
19
20create table public.group_invites (
21 id uuid primary key default gen_random_uuid(),
22 group_id uuid references public.groups(id) on delete cascade not null,
23 email text not null,
24 token uuid not null unique default gen_random_uuid(),
25 expires_at timestamptz not null default now() + interval '24 hours',
26 accepted_at timestamptz
27);
28
29-- Indexes
30create index group_members_user_idx on public.group_members (user_id);
31create index group_invites_token_idx on public.group_invites (token);
32create index groups_slug_idx on public.groups (slug);
33
34-- RLS
35alter table public.groups enable row level security;
36alter table public.group_members enable row level security;
37alter table public.group_invites enable row level security;
38
39-- Groups: members can read their groups
40create policy "Members can view their groups"
41 on public.groups for select
42 using (
43 exists (
44 select 1 from public.group_members
45 where group_id = groups.id
46 and user_id = auth.uid()
47 )
48 );
49
50-- Groups: authenticated users can create groups
51create policy "Authenticated users can create groups"
52 on public.groups for insert
53 with check (auth.uid() is not null);
54
55-- Groups: owners and admins can update group details
56create policy "Owners and admins can update groups"
57 on public.groups for update
58 using (
59 exists (
60 select 1 from public.group_members
61 where group_id = groups.id
62 and user_id = auth.uid()
63 and role in ('owner', 'admin')
64 )
65 );
66
67-- Group members: members can view the member list
68create policy "Members can view group members"
69 on public.group_members for select
70 using (
71 exists (
72 select 1 from public.group_members gm2
73 where gm2.group_id = group_members.group_id
74 and gm2.user_id = auth.uid()
75 )
76 );
77
78-- Group invites: owners and admins can insert invites
79create policy "Owners and admins can create invites"
80 on public.group_invites for insert
81 with check (
82 exists (
83 select 1 from public.group_members
84 where group_id = group_invites.group_id
85 and user_id = auth.uid()
86 and role in ('owner', 'admin')
87 )
88 );

Heads up: The primary key on group_members (group_id, user_id) acts as the UNIQUE constraint preventing duplicate membership rows. The group_role enum keeps role values strictly controlled — no typo can insert an invalid role.

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:

Best all-round path — Lovable auto-wires Supabase tables, RLS policies, and Supabase Auth in one project. The invite email system needs one additional prompt to configure the Resend API key.

Step by step

  1. 1Create a new Lovable project and connect Lovable Cloud so Supabase Auth and the database are provisioned automatically
  2. 2Paste the prompt below in Agent Mode — let it generate the groups list, creation modal, member management panel, and invite flow in one session
  3. 3Open Cloud tab → Secrets and add RESEND_API_KEY with your Resend API key so invite emails can be sent from the Edge Function
  4. 4Add a second prompt: 'Wire group avatar upload to Supabase Storage — create a group-avatars bucket with public read access and update avatar_url on the groups row after upload'
  5. 5Click Publish → Update, then test the invite flow on the live URL — Edge Function outbound email calls are blocked in the Lovable preview iframe
Paste into Lovable
Build a user groups feature on top of Supabase. Create three tables: groups (id uuid, name text 3-50 chars, slug text unique auto-generated from name, avatar_url text, created_by uuid, created_at), group_members (group_id, user_id, role enum owner/admin/member, joined_at) with PRIMARY KEY (group_id, user_id), and group_invites (id, group_id, email, token uuid unique, expires_at 24h from now, accepted_at). Enable RLS: members can SELECT their own groups via EXISTS check on group_members; only owners and admins can UPDATE group details or INSERT invites. UI: a Groups list page showing my groups as cards with member count badge and empty state; a Create Group modal with name input (3-50 chars, real-time slug preview), optional avatar upload to Supabase Storage group-avatars bucket; a Group Detail page with a TanStack Table member list showing avatar, username, role dropdown (with confirm dialog for role change), and Remove button; block remove if the user is the only owner and show 'Transfer ownership first' error. Invite flow: email input field, POST to a Supabase Edge Function that inserts a group_invites row and calls Resend API (key from env RESEND_API_KEY) to send an invite email with a link to /invite/:token; the invite page validates token not expired and not already accepted, then inserts a group_members row with role 'member'. Add search bar to filter groups list by name. Supabase Realtime Presence on a channel keyed by group_id shows green online dot next to members; call channel.unsubscribe() on unmount.

Where this path bites

  • Invite email delivery only works on the published URL — Lovable preview iframe blocks Edge Function outbound calls
  • Group avatar upload requires a second explicit prompt; Lovable rarely wires Storage in the first pass
  • Real-time presence on groups with 50+ simultaneous online members may require upgrading Supabase Realtime channel limits

Third-party services you'll need

Two services cover the full feature. Supabase provides the database, RLS, auth, and Realtime. Resend delivers invite emails.

ServiceWhat it doesFree tierPaid from
SupabasePostgreSQL database for groups/members/invites tables, Row Level Security, Supabase Auth, Realtime Presence channelsFree: 2 projects, 500MB DB, 200 concurrent Realtime connectionsPro: $25/mo (8GB DB, 500 concurrent Realtime)
ResendTransactional invite emails — sends the tokenized invite link to the invited user's email addressFree: 3,000 emails/month$20/mo for 50,000 emails/month (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 covers the database, auth, and Realtime at this scale. Resend free tier handles invite volume easily.

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.

Group data is world-readable because RLS has no member policy

Symptom: AI tools frequently generate the groups table and then apply RLS without writing the SELECT policy that actually checks membership. The result: enabling RLS with no policies blocks all reads entirely, or the AI leaves RLS disabled, making group data public to any authenticated user.

Fix: After the AI generates the schema, open the Supabase Table Editor → groups → Policies and verify a SELECT policy exists using the EXISTS check on group_members. The SQL in the data model section above includes the correct policy — paste it into the SQL Editor if missing.

Invite emails never arrive when testing in Lovable preview

Symptom: Supabase Edge Functions make outbound HTTP calls to Resend, but the Lovable preview iframe sandboxes network requests. The Edge Function appears to succeed (no error shown) but the email is never delivered.

Fix: Always test the invite email flow on the published URL, not the preview pane. Click Publish → Update in Lovable, then use the live URL for all invite testing.

The last owner can leave the group, locking everyone out

Symptom: AI-generated remove-member logic treats the owner role like any other member. When the only owner deletes their own group_members row, the group still exists in the database but no one can manage, delete, or invite to it — it becomes an orphan.

Fix: Add a server-side guard in your Edge Function or RLS UPDATE policy: before processing a leave or role-change request, count owners. If count = 1 and the action would remove or downgrade the last owner, return a 400 error with the message 'Transfer ownership before leaving.' Show this in the UI with a modal prompting ownership transfer.

Duplicate membership rows appear on double-click

Symptom: Without a UNIQUE constraint on (group_id, user_id), a user who double-clicks 'Accept Invite' or whose invite link is opened twice gets two rows in group_members. They appear twice in the member list and their permissions may behave inconsistently.

Fix: The PRIMARY KEY (group_id, user_id) in the data model SQL above prevents duplicates at the database level. If you see this bug, confirm the constraint exists by running: SELECT constraint_name FROM information_schema.table_constraints WHERE table_name = 'group_members'.

Realtime presence leaks and keeps members showing as online after navigation

Symptom: Supabase Presence channels maintain state until explicitly unsubscribed. If the React component mounts a Presence channel in useEffect but never calls channel.unsubscribe() on unmount, users remain 'online' in other members' views even after leaving the page.

Fix: Always return a cleanup function from useEffect: `return () => { channel.unsubscribe(); }`. Say this explicitly in your prompt — AI tools omit the cleanup more than half the time.

Best practices

1

Enforce slug uniqueness server-side with a UNIQUE constraint — client-only validation allows two groups to claim the same slug under concurrent requests

2

Auto-generate the slug from the group name on creation but let users edit it once before saving — prevents ugly auto-slugs like 'my-awesome-team-1'

3

Block deletion of the last owner membership at the database level with a trigger or Edge Function guard, not just in the UI

4

Expire invite tokens after 24 hours by default but allow admins to re-send — re-send generates a fresh token, not a duplicate row

5

Scope all group-content tables with an EXISTS check on group_members — not just the groups table itself

6

Show a member count badge on group cards in the list so users can see group size before entering

7

Log role changes to an audit table (group_id, changed_by, target_user_id, old_role, new_role, changed_at) — founders ask for this retroactively after the first moderation incident

8

Paginate the groups list if a user can belong to more than 50 groups — use cursor-based pagination keyed on joined_at to avoid offset drift

When You Need Custom Development

AI tools handle flat groups with roles confidently. Certain organizational requirements go beyond what a prompt session can reliably deliver:

  • Your app needs nested groups — sub-teams inside teams — with permission inheritance flowing down the hierarchy (requires a closure table or materialized path in PostgreSQL)
  • Each group has its own billing subscription: groups pay separately, have their own Stripe Customer, and members are billed per-seat within the group
  • You need a group analytics dashboard showing member activity heatmaps, post volume trends, and cohort retention — not just a member count
  • Complex permission inheritance where org-level roles override group-level roles and admins can grant cross-group access

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 limit how many groups one user can create?

Add a check in your group creation Edge Function or Server Action: `SELECT COUNT(*) FROM groups WHERE created_by = auth.uid()` and return a 400 error if it exceeds your limit (e.g., 10 groups). For a soft limit with upgrade prompts, store the limit on the user's profile row and check it before insert.

Can groups have sub-groups or nested teams?

Not with the flat schema above — you'd need either a parent_group_id self-reference (single-level nesting only) or a PostgreSQL closure table for arbitrary depth. Nested groups with inherited permissions are a meaningful engineering scope increase; AI tools rarely implement them correctly without custom development.

How do I notify all group members at once?

The simplest pattern: an Edge Function that queries group_members for the target group_id, then calls Resend with a batch of recipient emails. For in-app notifications, insert a row per member into a notifications table and use Supabase Realtime to push them to online members. For push notifications on mobile, call your push provider (Expo Push or FCM) with the device tokens stored per user.

What's the best way to handle group avatars?

Upload to Supabase Storage in a public bucket named group-avatars. Store the returned public URL in the groups.avatar_url column. On the client, display via a shadcn/ui Avatar component with a fallback showing the first letter of the group name. Resize images client-side to 256x256 before upload using the browser's Canvas API to keep storage costs low.

Can users belong to multiple groups at once?

Yes — that's exactly what the group_members join table enables. Each row represents one user's membership in one group. There's no structural limit; enforce a per-user maximum in application logic if your business model requires it.

How do I transfer group ownership?

Build a two-step UI: the current owner selects a new owner from the member list and confirms. Server-side, run both updates in a single Supabase transaction: UPDATE group_members SET role = 'owner' WHERE user_id = new_owner AND group_id = ...; UPDATE group_members SET role = 'admin' WHERE user_id = current_owner AND group_id = .... Using a transaction ensures you can never end up with zero or two owners.

How do I archive a group without deleting its content?

Add an archived_at timestamptz column (nullable) to the groups table. Archiving sets it to NOW(); unarchiving sets it to NULL. Filter archived groups from the active list using WHERE archived_at IS NULL. All content rows remain untouched. Block new invites and membership changes to archived groups via an RLS UPDATE policy checking that archived_at IS NULL.

Can groups be public (discoverable) vs private?

Add a visibility column to groups: 'public' allows any authenticated user to discover and request to join via a directory page; 'private' restricts all reads to existing members via the RLS EXISTS check. The groups directory page queries only WHERE visibility = 'public' with an anon-accessible SELECT policy, so no authentication is required to browse public groups.

RapidDev

Need this feature production-ready?

RapidDev builds user groups 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.