Blog

How To Design Multi-Tenant RBAC with Supabase RLS and Next.js Middleware

A practical MSIS-aligned model for tenant isolation, role controls, and safe privileged operations in a Next.js + Supabase SaaS.

How To Design Multi-Tenant RBAC with Supabase RLS and Next.js Middleware
1 min read2026-04-28
By Published Updated

How To Design Multi-Tenant RBAC with Supabase RLS and Next.js Middleware

Use RLS as the first boundary and middleware as the second boundary. In this platform, SQL policies enforce tenant and role constraints per table, while middleware routes users by role and subscription status. Service-role clients are limited to specific server-only workflows like webhooks and system notifications.

RLS and Middleware Security Model

The SQL policy layer

CREATE POLICY "Staff can view their own appointments"
  ON public.appointments FOR SELECT
  USING (
    get_my_role() IN ('barber', 'receptionist')
    AND staff_id = auth.uid()
    AND salon_id = get_my_salon_id()
  );
CREATE POLICY "Owners can view their salon's credits"
  ON public.salon_credits FOR SELECT
  USING (
    get_my_role() = 'owner'
    AND salon_id = get_my_salon_id()
  );

The middleware layer

if (pathname.startsWith("/dashboard") || pathname === "/onboarding") {
  if (isCustomer) return NextResponse.redirect(new URL("/portal", request.url));
  if (isOwnerOrStaff && profile?.salon_id) {
    const { data: salon } = await supabase
      .from("salons")
      .select("subscription_status, subscription_plan, stripe_customer_id")
      .eq("id", profile.salon_id)
      .single();
    const hasActiveSubscription =
      ["active", "trialing"].includes(salon?.subscription_status || "") ||
      (salon?.subscription_plan === "classic" && !!salon?.stripe_customer_id);
    if (!hasActiveSubscription) {
      return NextResponse.redirect(new URL("/dashboard/settings/billing", request.url));
    }
  }
}

MSIS takeaway

  • Put tenant and row ownership in SQL (auth.uid(), get_my_salon_id()).
  • Put route and journey decisions in middleware.
  • Keep privileged bypasses explicit, narrow, and server-only.

This dual-layer model is what keeps security understandable under rapid product iteration.

Read the full implementation context in the case study: /case-studies/cooard-salon-platform

Related reading

Why We Used App Router Server Components for a Multi-Tenant Salon Platform

How route groups, server-side data orchestration, and middleware produced predictable role-aware UX at scale.

Continue reading

How We Used Agentic Workflows and AI Automation Without Risking Core Transactions

A blueprint for using AI in delivery and content automation while keeping booking, billing, and access control deterministic.

Continue reading

How I Implemented Stripe Subscriptions and Credit Top-Ups in Next.js 16

A production pattern for combining Stripe subscriptions, one-time credit purchases, idempotent webhook fulfillment, and Supabase-backed access control.

Continue reading