Skip to main content

Overview

The authentication schema manages user accounts, roles, profiles, and third-party identity provider integrations including Zitadel OIDC/SAML.

Tables

profiles

Extends auth.users with organization membership and role information.
Schema
CREATE TABLE public.profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE,
  full_name TEXT NOT NULL,
  role public.user_role DEFAULT 'user',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Columns

id
UUID
required
Primary key, references auth.users(id). User’s unique identifier.
organization_id
UUID
Foreign key to organizations. User’s organization membership.
full_name
TEXT
required
User’s full name or display name.
role
user_role
default:"user"
User’s role: global_admin, org_admin, support, or user.
created_at
TIMESTAMPTZ
Profile creation timestamp.

Indexes

  • idx_profiles_organization_id - Fast lookups by organization
  • idx_profiles_role - Fast filtering by role

Triggers

on_auth_user_created - Automatically creates a profile when a new user signs up:
Trigger Function
CREATE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, full_name, role)
  VALUES (
    NEW.id,
    COALESCE(NEW.raw_user_meta_data->>'full_name', 'New User'),
    'user'::user_role
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

user_roles

Separate table for role assignments (supports multiple roles per user).
Schema
CREATE TABLE public.user_roles (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  role TEXT NOT NULL,
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(user_id, role)
);

Columns

id
UUID
Primary key
user_id
UUID
required
User ID from auth.users
role
TEXT
required
Role name (e.g., global_admin, org_admin, support, user)
created_at
TIMESTAMPTZ
Role assignment timestamp

Constraints

  • Unique constraint: (user_id, role) - Prevents duplicate role assignments

zitadel_configurations

Stores Zitadel OIDC/SAML configuration per organization.
Schema
CREATE TABLE public.zitadel_configurations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID NOT NULL REFERENCES public.organizations(id),
  name TEXT NOT NULL,
  issuer_url TEXT NOT NULL,
  client_id TEXT NOT NULL,
  client_secret TEXT,
  redirect_uri TEXT NOT NULL,
  scopes TEXT[] DEFAULT ARRAY['openid', 'profile', 'email', 'groups'],
  api_token TEXT,
  project_id TEXT,
  sync_groups BOOLEAN DEFAULT true,
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(organization_id, name)
);

Columns

id
UUID
Configuration ID
organization_id
UUID
required
Organization this config belongs to
name
TEXT
required
Configuration name (e.g., “Production Zitadel”)
issuer_url
TEXT
required
Zitadel issuer URL (e.g., https://gate.kappa4.com)
client_id
TEXT
required
OAuth client ID
client_secret
TEXT
OAuth client secret (encrypted in backend)
redirect_uri
TEXT
required
OAuth redirect URI after authentication
scopes
TEXT[]
OAuth scopes requested (default: openid, profile, email, groups)
api_token
TEXT
Zitadel Management API token for group sync
project_id
TEXT
Zitadel project ID to scope roles/grants
sync_groups
BOOLEAN
default:"true"
Whether to automatically sync groups from Zitadel
is_active
BOOLEAN
default:"true"
Whether this configuration is active for SSO
created_at
TIMESTAMPTZ
Creation timestamp
updated_at
TIMESTAMPTZ
Last update timestamp

zitadel_group_mappings

Maps Zitadel roles/groups to local groups.
Schema
CREATE TABLE public.zitadel_group_mappings (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  zitadel_config_id UUID NOT NULL REFERENCES public.zitadel_configurations(id) ON DELETE CASCADE,
  zitadel_group_id TEXT NOT NULL,
  zitadel_group_name TEXT NOT NULL,
  local_group_id UUID REFERENCES public.groups(id) ON DELETE SET NULL,
  auto_sync BOOLEAN DEFAULT true,
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(zitadel_config_id, zitadel_group_id)
);

Columns

id
UUID
Mapping ID
zitadel_config_id
UUID
required
Zitadel configuration this mapping belongs to
zitadel_group_id
TEXT
required
Role/group ID from Zitadel
zitadel_group_name
TEXT
required
Role/group display name from Zitadel
local_group_id
UUID
Mapped local group ID (can be null if not mapped)
auto_sync
BOOLEAN
default:"true"
Whether to automatically sync members
created_at
TIMESTAMPTZ
Mapping creation timestamp

user_zitadel_identities

Links local users to their Zitadel identities.
Schema
CREATE TABLE public.user_zitadel_identities (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
  zitadel_config_id UUID NOT NULL REFERENCES public.zitadel_configurations(id) ON DELETE CASCADE,
  zitadel_user_id TEXT NOT NULL,
  zitadel_groups TEXT[],
  last_synced_at TIMESTAMPTZ DEFAULT now(),
  created_at TIMESTAMPTZ DEFAULT now(),
  UNIQUE(user_id, zitadel_config_id),
  UNIQUE(zitadel_config_id, zitadel_user_id)
);

Columns

id
UUID
Identity link ID
user_id
UUID
required
Local user ID
zitadel_config_id
UUID
required
Zitadel configuration used
zitadel_user_id
TEXT
required
User’s subject ID in Zitadel
zitadel_groups
TEXT[]
Current Zitadel groups/roles for this user
last_synced_at
TIMESTAMPTZ
Last time groups were synced
created_at
TIMESTAMPTZ
Identity link creation timestamp

Enums

user_role

Enum Definition
CREATE TYPE public.user_role AS ENUM (
  'global_admin',  -- Full system access
  'org_admin',     -- Organization administrator
  'support',       -- Support/helpdesk role
  'user'          -- Standard user
);

Helper Functions

get_user_role

Security definer function to get a user’s role (breaks RLS recursion).
Function
CREATE FUNCTION public.get_user_role(_user_id uuid)
RETURNS user_role
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = public
AS $$
  SELECT role FROM profiles WHERE id = _user_id
$$;

get_user_org_id

Security definer function to get a user’s organization ID.
Function
CREATE FUNCTION public.get_user_org_id(_user_id uuid)
RETURNS uuid
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = public
AS $$
  SELECT organization_id FROM profiles WHERE id = _user_id
$$;

get_active_sso_configs

Public function to retrieve active SSO configurations (safe fields only).
Function
CREATE FUNCTION public.get_active_sso_configs()
RETURNS TABLE(id uuid, name text, issuer_url text, organization_id uuid)
LANGUAGE sql STABLE SECURITY DEFINER
SET search_path = public
AS $$
  SELECT id, name, issuer_url, organization_id
  FROM zitadel_configurations
  WHERE is_active = true;
$$;

Row Level Security

All tables have RLS enabled. Key policies:

profiles

RLS Policies
-- Users can view profiles in their organization
CREATE POLICY "Users can view profiles in their organization"
ON public.profiles FOR SELECT
USING (
  organization_id = public.get_user_org_id(auth.uid())
  OR public.get_user_role(auth.uid()) = 'global_admin'
);

-- Users can view their own profile
CREATE POLICY "Users can view their own profile"
ON public.profiles FOR SELECT
USING (id = auth.uid());

-- Admins can update profiles
CREATE POLICY "Org admins can update profiles in their org"
ON public.profiles FOR UPDATE
USING (
  public.get_user_role(auth.uid()) IN ('org_admin', 'global_admin')
  AND organization_id = public.get_user_org_id(auth.uid())
);

zitadel_configurations

RLS Policies
-- Only admins can manage Zitadel configs
CREATE POLICY "Admins can manage Zitadel configs"
ON public.zitadel_configurations FOR ALL
USING (
  organization_id = get_user_org_id(auth.uid())
  AND get_user_role(auth.uid()) = ANY (ARRAY['org_admin'::user_role, 'global_admin'::user_role])
);
  • organizations - Organization records
  • groups - User groups for permissions
  • user_groups - User group memberships
  • audit_logs - Authentication audit trail

Build docs developers (and LLMs) love