Skip to content

Internal Groups

Internal Groups are the traditional database-managed group model where membership is explicitly stored and managed within the PostgreSQL database. This approach provides complete control over group membership and is ideal for application-specific roles and business groups.

Overview

Internal Groups use direct database storage for membership management:

graph LR
    subgraph "Internal Group Model"
        IG[Internal Group<br/>auth.user_group<br/>group_type = 'internal']
        GM[Group Members<br/>auth.user_group_member<br/>Direct membership storage]

        U1[User 1]
        U2[User 2]
        U3[User 3]
    end

    GM --> U1
    GM --> U2
    GM --> U3
    IG --> GM

Key Characteristics: - Membership stored in auth.user_group_member table - Complete administrative control - No dependency on external systems - Immediate membership changes - Perfect for application-specific roles

Database Structure

Core Tables

-- Internal group definition
CREATE TABLE auth.user_group (
    user_group_id uuid PRIMARY KEY,
    tenant_id uuid REFERENCES auth.tenant,
    code text NOT NULL,
    name text NOT NULL,
    description text,
    group_type text DEFAULT 'internal',
    is_active boolean DEFAULT true,
    created_at timestamptz DEFAULT now(),
    UNIQUE (tenant_id, code)
);

-- Direct membership storage
CREATE TABLE auth.user_group_member (
    user_group_id uuid REFERENCES auth.user_group,
    user_id uuid REFERENCES auth.user_info,
    added_at timestamptz DEFAULT now(),
    added_by uuid REFERENCES auth.user_info,
    is_active boolean DEFAULT true,
    PRIMARY KEY (user_group_id, user_id)
);

Creating Internal Groups

Basic Group Creation

-- Create internal group
SELECT auth.create_user_group(
    _tenant_id := 'tenant-uuid',
    _code := 'DEVELOPERS',
    _name := 'Development Team',
    _description := 'Software developers and engineers',
    _group_type := 'internal'
);
-- Returns: user_group_id (uuid)

-- Simpler creation (defaults to internal)
SELECT auth.create_user_group(
    'tenant-uuid',
    'MANAGERS',
    'Management Team',
    'Department managers and team leads'
);

Common Internal Group Examples

-- Department-based groups
SELECT auth.create_user_group(
    'tenant-uuid', 'ENGINEERING', 'Engineering Department',
    'All engineering staff'
);

SELECT auth.create_user_group(
    'tenant-uuid', 'MARKETING', 'Marketing Department',
    'Marketing and communications team'
);

SELECT auth.create_user_group(
    'tenant-uuid', 'FINANCE', 'Finance Department',
    'Accounting and financial analysts'
);

-- Role-based groups
SELECT auth.create_user_group(
    'tenant-uuid', 'TEAM_LEADS', 'Team Leaders',
    'Team leads across all departments'
);

SELECT auth.create_user_group(
    'tenant-uuid', 'SENIOR_STAFF', 'Senior Staff',
    'Senior level employees'
);

-- Function-based groups
SELECT auth.create_user_group(
    'tenant-uuid', 'APPROVERS', 'Expense Approvers',
    'Users who can approve expenses'
);

Managing Group Membership

Adding Members

-- Add single member to group
SELECT auth.create_user_group_member(
    _user_group_id := 'group-uuid',
    _user_id := 'user-uuid',
    _added_by := 'admin-user-uuid'  -- Who added them
);

-- Add multiple members
DO $$
DECLARE
    v_group_id uuid := 'developers-group-uuid';
    v_admin_id uuid := 'admin-user-uuid';
    v_user_ids uuid[] := ARRAY[
        'john-uuid', 'jane-uuid', 'bob-uuid'
    ];
    v_user_id uuid;
BEGIN
    FOREACH v_user_id IN ARRAY v_user_ids
    LOOP
        PERFORM auth.create_user_group_member(v_group_id, v_user_id, v_admin_id);
    END LOOP;
END $$;

Removing Members

-- Remove member from group (soft delete)
SELECT auth.deactivate_user_group_member(
    _user_group_id := 'group-uuid',
    _user_id := 'user-uuid'
);

-- Hard delete member
SELECT auth.delete_user_group_member(
    _user_group_id := 'group-uuid',
    _user_id := 'user-uuid'
);

Bulk Operations

-- Add all users from one group to another
INSERT INTO auth.user_group_member (user_group_id, user_id, added_by)
SELECT
    'target-group-uuid',
    ugm.user_id,
    'admin-user-uuid'
FROM auth.user_group_member ugm
WHERE ugm.user_group_id = 'source-group-uuid'
  AND ugm.is_active = true
ON CONFLICT (user_group_id, user_id) DO NOTHING;

-- Move all members from one group to another
UPDATE auth.user_group_member
SET user_group_id = 'new-group-uuid'
WHERE user_group_id = 'old-group-uuid';

Group Administration

Update Group Information

-- Update group details
SELECT auth.update_user_group(
    _user_group_id := 'group-uuid',
    _name := 'Updated Group Name',
    _description := 'Updated description with more details'
);

-- Activate/deactivate group
UPDATE auth.user_group
SET is_active = false
WHERE user_group_id = 'group-uuid';

Group Hierarchy

Internal groups can reference other groups for hierarchical structures:

-- Create parent-child group relationships
CREATE TABLE auth.user_group_hierarchy (
    parent_group_id uuid REFERENCES auth.user_group(user_group_id),
    child_group_id uuid REFERENCES auth.user_group(user_group_id),
    created_at timestamptz DEFAULT now(),
    PRIMARY KEY (parent_group_id, child_group_id)
);

-- Example: Managers group contains Team Leads
INSERT INTO auth.user_group_hierarchy (parent_group_id, child_group_id)
VALUES (
    (SELECT user_group_id FROM auth.user_group WHERE code = 'MANAGERS'),
    (SELECT user_group_id FROM auth.user_group WHERE code = 'TEAM_LEADS')
);

Permission Assignment to Internal Groups

Assign Permission Sets

-- Assign permission set to internal group
SELECT auth.assign_permission(
    _tenant_id := 'tenant-uuid',
    _user_id := NULL,
    _user_group_id := 'developers-group-uuid',
    _perm_set_code := 'DEVELOPER_PERMISSIONS',
    _perm_code := NULL
);

-- All group members inherit these permissions

Assign Individual Permissions

-- Assign specific permission to group
SELECT auth.assign_permission(
    _tenant_id := 'tenant-uuid',
    _user_id := NULL,
    _user_group_id := 'approvers-group-uuid',
    _perm_set_code := NULL,
    _perm_code := 'expenses.approve'
);

Multiple Permission Assignment

-- Assign multiple permission sets to group
DO $$
DECLARE
    v_group_id uuid := (
        SELECT user_group_id FROM auth.user_group
        WHERE code = 'SENIOR_STAFF' AND tenant_id = 'tenant-uuid'
    );
    v_permission_sets text[] := ARRAY['BASE_USER', 'ADVANCED_FEATURES', 'REPORTING'];
    v_perm_set text;
BEGIN
    FOREACH v_perm_set IN ARRAY v_permission_sets
    LOOP
        PERFORM auth.assign_permission(
            'tenant-uuid', NULL, v_group_id, v_perm_set, NULL
        );
    END LOOP;
END $$;

Querying Internal Groups

Group Membership

-- Get all members of a group
SELECT
    u.user_id,
    u.username,
    u.display_name,
    ugm.added_at,
    ugm.added_by,
    adder.display_name as added_by_name
FROM auth.user_group_member ugm
JOIN auth.user_info u ON ugm.user_id = u.user_id
LEFT JOIN auth.user_info adder ON ugm.added_by = adder.user_id
WHERE ugm.user_group_id = 'group-uuid'
  AND ugm.is_active = true
ORDER BY ugm.added_at;

User's Group Memberships

-- Get all groups a user belongs to
SELECT
    ug.code,
    ug.name,
    ug.description,
    ugm.added_at
FROM auth.user_group_member ugm
JOIN auth.user_group ug ON ugm.user_group_id = ug.user_group_id
WHERE ugm.user_id = 'user-uuid'
  AND ugm.is_active = true
  AND ug.is_active = true
ORDER BY ug.name;

Group Statistics

-- Group membership statistics
SELECT
    ug.code,
    ug.name,
    COUNT(ugm.user_id) as member_count,
    COUNT(ugm.user_id) FILTER (WHERE ugm.added_at > now() - interval '30 days') as recent_additions,
    MIN(ugm.added_at) as first_member_added,
    MAX(ugm.added_at) as last_member_added
FROM auth.user_group ug
LEFT JOIN auth.user_group_member ugm ON ug.user_group_id = ugm.user_group_id
    AND ugm.is_active = true
WHERE ug.tenant_id = 'tenant-uuid'
  AND ug.group_type = 'internal'
  AND ug.is_active = true
GROUP BY ug.user_group_id
ORDER BY member_count DESC;

Common Internal Group Patterns

Department-Based Access Control

-- Create department groups with appropriate permissions
DO $$
DECLARE
    v_tenant_id uuid := 'tenant-uuid';
    v_dept_groups record;
BEGIN
    -- Create department groups
    FOR v_dept_groups IN
        SELECT * FROM (VALUES
            ('HR', 'Human Resources', 'HR_PERMISSIONS'),
            ('IT', 'Information Technology', 'IT_ADMIN'),
            ('SALES', 'Sales Team', 'SALES_PERMISSIONS'),
            ('SUPPORT', 'Customer Support', 'SUPPORT_PERMISSIONS')
        ) AS depts(code, name, perm_set)
    LOOP
        -- Create group
        PERFORM auth.create_user_group(
            v_tenant_id,
            v_dept_groups.code,
            v_dept_groups.name,
            v_dept_groups.name || ' department members'
        );

        -- Assign permission set
        PERFORM auth.assign_permission(
            v_tenant_id,
            NULL,
            (SELECT user_group_id FROM auth.user_group
             WHERE code = v_dept_groups.code AND tenant_id = v_tenant_id),
            v_dept_groups.perm_set,
            NULL
        );
    END LOOP;
END $$;

Role-Based Groups

-- Create role-based groups with hierarchical permissions
DO $$
DECLARE
    v_tenant_id uuid := 'tenant-uuid';
BEGIN
    -- Admin group (highest level)
    PERFORM auth.create_user_group(
        v_tenant_id, 'ADMINS', 'Administrators', 'System administrators'
    );
    PERFORM auth.assign_permission(
        v_tenant_id, NULL,
        (SELECT user_group_id FROM auth.user_group WHERE code = 'ADMINS'),
        'FULL_ADMIN', NULL
    );

    -- Manager group (mid level)
    PERFORM auth.create_user_group(
        v_tenant_id, 'MANAGERS', 'Managers', 'Department managers'
    );
    PERFORM auth.assign_permission(
        v_tenant_id, NULL,
        (SELECT user_group_id FROM auth.user_group WHERE code = 'MANAGERS'),
        'MANAGER_PERMISSIONS', NULL
    );

    -- Employee group (base level)
    PERFORM auth.create_user_group(
        v_tenant_id, 'EMPLOYEES', 'Employees', 'All employees'
    );
    PERFORM auth.assign_permission(
        v_tenant_id, NULL,
        (SELECT user_group_id FROM auth.user_group WHERE code = 'EMPLOYEES'),
        'BASIC_USER', NULL
    );
END $$;

Project-Based Groups

-- Dynamic project groups
CREATE OR REPLACE FUNCTION create_project_group(
    _tenant_id uuid,
    _project_code text,
    _project_name text,
    _project_lead_user_id uuid
) RETURNS uuid AS $$
DECLARE
    v_group_id uuid;
BEGIN
    -- Create project group
    v_group_id := auth.create_user_group(
        _tenant_id,
        'PROJECT_' || upper(_project_code),
        'Project: ' || _project_name,
        'Members of project ' || _project_name
    );

    -- Add project lead
    PERFORM auth.create_user_group_member(
        v_group_id, _project_lead_user_id, _project_lead_user_id
    );

    -- Assign project permissions
    PERFORM auth.assign_permission(
        _tenant_id, NULL, v_group_id, 'PROJECT_MEMBER', NULL
    );

    RETURN v_group_id;
END;
$$ LANGUAGE plpgsql;

-- Usage
SELECT create_project_group(
    'tenant-uuid',
    'ALPHA',
    'Project Alpha',
    'project-lead-user-uuid'
);

Best Practices for Internal Groups

Group Design Principles

  1. Clear Purpose: Each group should have a specific, well-defined purpose
  2. Meaningful Names: Use descriptive codes and names
  3. Appropriate Granularity: Not too broad, not too narrow
  4. Document Membership Criteria: Clear rules for who belongs

Membership Management

-- Audit group membership changes
CREATE TABLE auth.user_group_member_audit (
    audit_id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_group_id uuid,
    user_id uuid,
    action text, -- 'ADDED', 'REMOVED', 'ACTIVATED', 'DEACTIVATED'
    performed_by uuid REFERENCES auth.user_info(user_id),
    performed_at timestamptz DEFAULT now(),
    reason text
);

-- Function to audit membership changes
CREATE OR REPLACE FUNCTION audit_group_membership_change(
    _user_group_id uuid,
    _user_id uuid,
    _action text,
    _performed_by uuid,
    _reason text DEFAULT NULL
) RETURNS void AS $$
BEGIN
    INSERT INTO auth.user_group_member_audit
        (user_group_id, user_id, action, performed_by, reason)
    VALUES
        (_user_group_id, _user_id, _action, _performed_by, _reason);
END;
$$ LANGUAGE plpgsql;

Performance Optimization

-- Essential indexes for internal groups
CREATE INDEX idx_user_group_member_user_lookup
    ON auth.user_group_member(user_id) WHERE is_active = true;

CREATE INDEX idx_user_group_member_group_lookup
    ON auth.user_group_member(user_group_id) WHERE is_active = true;

CREATE INDEX idx_user_group_tenant_type
    ON auth.user_group(tenant_id, group_type) WHERE is_active = true;

Security Considerations

  1. Access Control: Limit who can modify group membership
  2. Audit Trail: Log all membership changes
  3. Regular Reviews: Periodic membership audits
  4. Principle of Least Privilege: Only assign necessary permissions

Troubleshooting Internal Groups

Common Issues

Duplicate Membership:

-- Find and fix duplicate memberships
SELECT user_group_id, user_id, COUNT(*)
FROM auth.user_group_member
GROUP BY user_group_id, user_id
HAVING COUNT(*) > 1;

-- Clean up duplicates (keep most recent)
DELETE FROM auth.user_group_member ugm1
WHERE ugm1.ctid NOT IN (
    SELECT DISTINCT ON (user_group_id, user_id) ctid
    FROM auth.user_group_member ugm2
    WHERE ugm2.user_group_id = ugm1.user_group_id
      AND ugm2.user_id = ugm1.user_id
    ORDER BY user_group_id, user_id, added_at DESC
);

Orphaned Memberships:

-- Find memberships for inactive users or groups
SELECT
    ugm.user_group_id,
    ugm.user_id,
    'Inactive user' as issue
FROM auth.user_group_member ugm
JOIN auth.user_info u ON ugm.user_id = u.user_id
WHERE u.is_active = false

UNION ALL

SELECT
    ugm.user_group_id,
    ugm.user_id,
    'Inactive group' as issue
FROM auth.user_group_member ugm
JOIN auth.user_group ug ON ugm.user_group_id = ug.user_group_id
WHERE ug.is_active = false;

What's Next