Skip to main content

Multi-Tenancy Architecture

Tonnex uses a Table-Based Multi-Tenancy approach with Row Level Security (RLS). Every tenant-specific table has an org_id column that links to the organizations table. Access is enforced at the database level using Postgres RLS policies.

Key Components

1. Organization Membership Check

We use a SECURITY DEFINER function to verify if a user belongs to an organization. This bypasses RLS on the org_memberships table itself to avoid infinite recursion.
CREATE OR REPLACE FUNCTION public.is_org_member(_org_id uuid)
RETURNS boolean AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1 FROM public.org_memberships 
    WHERE user_id = auth.uid() 
      AND org_id = _org_id
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER STABLE;

2. Drizzle Integration (Supabase)

We use drizzle-orm/supabase helpers to define RLS policies directly in the schema.
import { pgPolicy } from "drizzle-orm/pg-core";
import { authenticatedRole, isOrgMember } from "./rls";

export const projects = pgTable("projects", {
  // ... columns
  orgId: uuid("org_id").references(() => organizations.id),
}, (table) => [
  // Policy: Users can only see projects in their organizations
  pgPolicy("view_projects", {
    for: "select",
    to: authenticatedRole,
    using: isOrgMember(table.orgId),
  }),
]);

3. RLS-Aware Client

When querying from the application (Server Actions/Components), we use a wrapper that sets the RLS context for the transaction.
import { createDrizzleSupabaseClient } from "@tonnex/db";

// In a Server Action
const db = createDrizzleSupabaseClient(accessToken);
const data = await db.rls((tx) => tx.select().from(projects));
This sets:
  • auth.uid() via request.jwt.claim.sub
  • auth.jwt() via request.jwt.claims
  • role via set local role

Adding a New Module

When creating a new module (e.g., vehicles):
  1. Add orgId to your table schema.
  2. Add the RLS policy usage isOrgMember(table.orgId) for SELECT/UPDATE/DELETE.
  3. Use the createDrizzleSupabaseClient wrapper for all queries.