DEV Community

Shivam Kumar
Shivam Kumar

Posted on

Gym CRM SaaS: a multi-tenant Supabase schema with 35 RLS policies — code tour

A Next.js 14 + TypeScript + Supabase gym CRM, open source, multi-tenant by design. I read the schema table by table and walked the route tree. Here's the engineering worth studying if you build multi-tenant apps on Supabase.

Route structure: roles as route subtrees

src/app/
  owner/       dashboard members leads payments plans trainers attendance retention settings
  trainer/     dashboard members workouts diets
  member/      dashboard workout diet attendance payments profile renew
  superadmin/  dashboard
Enter fullscreen mode Exit fullscreen mode

Role-based access is structural — each role gets its own subtree under src/app/, with per-role layout.tsx files. Session handling follows the Supabase SSR pattern: src/middleware.ts matches all non-static routes and calls updateSession() from src/lib/supabase/middleware.ts, keeping auth cookies fresh across Server Components. Client/server Supabase helpers are split in src/lib/supabase/ (client.ts, server.ts, middleware.ts, plus generated types.ts).

The schema: 18 tables, one product story

supabase/schema.sql (~27KB). The tables:

-- Tenancy / SaaS layer
subscription_plans, gyms, gym_subscriptions, profiles, gym_settings
-- Memberships
membership_plans, members, member_memberships
-- Operations
attendance, payments, trainers
-- Programming
workout_plans, workout_plan_items, diet_plans, diet_plan_items
-- Growth
leads, lead_followups, message_templates
Enter fullscreen mode Exit fullscreen mode

Notice the loops the schema closes: leads → lead_followups → members (acquisition), membership_plans → member_memberships → payments → attendance (the money loop), workout_plans → workout_plan_items / diet_plans → diet_plan_items (trainer-authored programming consumed by members). And message_templates is the store behind the WhatsApp retention system.

35 RLS policies: isolation as a database guarantee

This is the part to copy. Tenant isolation isn't trusted to application code — 35 Row Level Security policies filter every tenant-scoped table by the authenticated user's gym. A UI bug can't leak Gym A's members to Gym B, because Postgres won't return the rows. If you're building multi-tenant on Supabase, this repo is a solid reference for what "RLS done thoroughly" looks like.

Zero hardcoded business numbers

The README's claim checks out structurally: pricing, plan durations, and reminder rules live in gym_settings, editable per gym. That's the line between software and a configurable product — and it's what makes one deployment serve gyms with different pricing.

The WhatsApp retention angle

src/lib/utils/whatsapp.ts builds tap-to-send messages from the message_templates table — localised per gym, composed with member data. The product thesis: members churn silently, and the win-back nudge is the highest-ROI feature in gym software. The owner panel even has a dedicated retention route for at-risk members.

Stack and setup

Lean package.json: next 14.1.0, @supabase/ssr + @supabase/supabase-js, react-hook-form + zod (validated forms), recharts (dashboards), date-fns, lucide-react, Tailwind. No UI kit, no state framework — Context + Server Components.

# 1. Paste supabase/schema.sql into the Supabase SQL editor and run it
# 2. cp .env.example .env.local   # add Supabase URL + keys
# 3. npm install && npm run dev
Enter fullscreen mode Exit fullscreen mode

DEPLOYMENT.md covers production deployment.

Honest take

Complete, coherent v1: the role panels are real routes, the schema joins up end to end, and isolation is enforced at the database level. What's next is obvious from the schema's shape — payment-gateway wiring against payments (records exist; collection webhooks don't), automated renewal reminders driven by member_memberships expiry, and a member PWA on the existing member routes. As a reference architecture for multi-tenant Supabase apps, start with schema.sql and the RLS policies.

Repo: https://github.com/Shivay00001/gym-crm-saas

Top comments (0)