Skip to main content

Authentication

SecurAtlas uses Supabase Auth with JWT tokens. Users authenticate via:

Email/Password

Standard email sign-up with confirmation

OAuth

Google and Microsoft OAuth providers

SSO/SAML

Enterprise SSO via Supabase SAML

Auth Callback Flow

Browser → /auth/callback → exchange code for session → set cookies → redirect to dashboard
The callback route at app/auth/callback/route.ts exchanges the OAuth code for a Supabase session, sets the session cookies, and redirects the user to their appropriate dashboard (partner or tenant).
The auth callback route must have maxDuration set appropriately in Vercel to avoid 504 errors during OAuth token exchange. The default 10s may not be enough for slow OAuth providers.

Supabase Client Usage

Two server-side client constructors are available:
Authenticated client scoped to the current user’s session. RLS policies are enforced.
import { getSupabaseServerClient } from '@kit/supabase/server-client';

const client = getSupabaseServerClient();
const { data } = await client.from('tenant_controls').select('*');
// Only returns rows the user has access to via RLS
Never use the admin client for user-initiated requests. It bypasses all RLS policies and would expose other tenants’ data.

Multi-Tenancy Model

SecurAtlas supports two organizational types:
TypeDescriptionTables
Solo OrganizationSingle tenant, users are direct memberstenant_memberships
MSP PartnerPartner manages multiple tenantspartner_memberships, partner_tenant_links

Membership Tables

-- Direct tenant membership
tenant_memberships (
  user_id       uuid REFERENCES auth.users,
  tenant_id     uuid REFERENCES tenants,
  role          text, -- 'owner', 'admin', 'member'
  created_at    timestamptz
)

-- Partner membership (MSP users)
partner_memberships (
  user_id       uuid REFERENCES auth.users,
  partner_id    uuid REFERENCES partners,
  role          text, -- 'owner', 'admin', 'member'
  created_at    timestamptz
)

-- Partner manages tenant
partner_tenant_links (
  partner_id    uuid REFERENCES partners,
  tenant_id     uuid REFERENCES tenants,
  created_at    timestamptz
)

Active Context Cookies

The current tenant/partner context is stored in cookies and read by middleware:
CookiePurpose
active_tenant_idCurrently selected tenant
active_partner_idCurrently selected partner (MSP users)

Middleware Session Refresh

The middleware.ts file runs on every request to:
  1. Refresh the Supabase session (prevents token expiry)
  2. Read active_tenant_id / active_partner_id cookies
  3. Redirect unauthenticated users to the sign-in page
  4. Redirect users without a valid membership to the account selector
// Simplified middleware flow
export async function middleware(request: NextRequest) {
  const response = NextResponse.next();
  const supabase = createMiddlewareClient(request, response);

  const { data: { session } } = await supabase.auth.getSession();

  if (!session) {
    return NextResponse.redirect(new URL('/auth/sign-in', request.url));
  }

  return response;
}
Cookie writes must happen on the response object, not on a detached cookieStore. Writing to cookieStore inside Route Handlers can cause deadlocks. Always use response.cookies.set().

Tenant Isolation via RLS

All tenant-scoped tables enforce isolation through Row Level Security:
-- Example RLS policy on tenant_controls
CREATE POLICY "Users can view controls for their tenant"
  ON tenant_controls FOR SELECT
  USING (
    tenant_id IN (
      SELECT tenant_id FROM tenant_memberships
      WHERE user_id = auth.uid()
    )
    OR
    tenant_id IN (
      SELECT ptl.tenant_id FROM partner_tenant_links ptl
      JOIN partner_memberships pm ON pm.partner_id = ptl.partner_id
      WHERE pm.user_id = auth.uid()
    )
  );
This ensures that users only see data for tenants they belong to directly or through a partner relationship.