Guides

User Management & Feature Flags System - Implementation Todo

User Management & Feature Flags System - Implementation Todo

Overview

Implement a comprehensive user management system with role-based access control (RBAC), group-based organization, and group-level feature flags for Jan Server.

Architecture Decision:

  • Use Keycloak as the single source of truth for users, roles, and groups
  • No local cache/sync - Read directly from JWT claims
  • Feature flags stored in Keycloak group attributes and included in JWT
  • Jan Server only stores feature flag definitions and audit logs
  • Users inherit ALL feature flags from ALL their groups (additive inheritance)

Technology Stack

Backend

  • Language: Go (Golang)
  • Framework: Gin (HTTP router)
  • Database: PostgreSQL (audit logs & feature flag definitions only)
  • Keycloak Client: github.com/Nerzal/gocloak/v13 (Admin API for management operations only)
  • Authentication: JWT validation via JWKS
  • Circuit Breaker: github.com/sony/gobreaker (Keycloak resilience)

Frontend (Future)

  • Framework: React or Next.js
  • UI Library: shadcn/ui or Material-UI
  • State Management: React Query (for cache management)
  • Tables: TanStack Table (React Table)
  • Forms: React Hook Form + Zod validation

Infrastructure

  • Identity Provider: Keycloak (users, groups, roles, attributes)
  • API Gateway: Kong (already in use)
  • JWT Validation: JWKS endpoint from Keycloak
  • Observability: Prometheus + Grafana (already in use)

User Roles

Admin Role

  • Privileges: Full system access, can manage all users and groups
  • Identification:
    • Keycloak realm role: admin OR
    • Keycloak user attribute: is_admin: true
  • Source of Truth: Keycloak realm roles
  • Capabilities:
    • Create, read, update, delete users (via Keycloak Admin API)
    • Assign/revoke admin privileges
    • Manage user groups (via Keycloak Admin API)
    • Activate/deactivate user accounts
    • View all system users
    • Manage group feature flags (admin-only)

User Role

  • Privileges: Standard platform access
  • Identification: Default role for authenticated users
  • Source of Truth: Keycloak realm roles
  • Capabilities:
    • Access own profile
    • Use platform features (based on group feature flags)
    • Cannot manage other users
    • Cannot modify feature flags

User Groups

Source of Truth: All groups are managed in Keycloak. Jan Server reads group membership from JWT claims.

jan_group

  • Description: Internal Jan team members
  • Membership Criteria: Email domain @jan.ai OR @menlo.ai
  • Auto-assignment: ✅ Automatic via Keycloak event listeners or registration flow
  • Keycloak Setup: Create group in Keycloak realm
  • Default Feature Flags: Experimental model access enabled
    feature_flags:
      - experimental_models  # Can see all models including experimental ones

pilot_users

  • Description: Beta testers and early adopters
  • Membership Criteria: Manual assignment by admin
  • Auto-assignment: ❌ Manual only
  • Keycloak Setup: Create group in Keycloak realm
  • Default Feature Flags: Experimental model access enabled
    feature_flags:
      - experimental_models  # Can see experimental models for testing

standard

  • Description: Verified regular users
  • Membership Criteria: Verified email address
  • Auto-assignment: ✅ Automatic after email verification
  • Keycloak Setup: Create group in Keycloak realm
  • Default Feature Flags: None (stable models only)
    feature_flags: []  # Can only see stable/production models

guest

  • Description: Guest/temporary access
  • Membership Criteria: Guest login without full registration
  • Auto-assignment: ✅ Automatic for guest login flow
  • Keycloak Setup: Create group in Keycloak realm
  • Default Feature Flags: None (stable models only)
    feature_flags: []  # Can only see stable/production models

Feature Flags System

Overview

Feature flags allow fine-grained control over which features are available to users based on their group membership.

Current Implementation: Single feature flag for experimental model access

Design Principles:

  • Group-level only: Feature flags defined per group
  • Additive inheritance: Users get ALL flags from ALL their groups
  • Simple management: No user-level overrides, no global flags
  • Keycloak-first: Stored as Keycloak group attributes
  • JWT-based: All feature flags included in JWT, no server-side caching or database lookups needed

Feature Flag Architecture

Source of Truth: Keycloak group attributes

Resolution Logic:

// User has a feature if ANY of their groups has that feature
// All data comes from JWT claims - no database queries
func IsFeatureEnabled(c *gin.Context, flagKey string) bool {
    claims := getJWTClaims(c) // Already validated by auth middleware
    
    // Defensive parsing of feature_flags array from JWT
    featureFlagsRaw, ok := claims["feature_flags"]
    if !ok {
        return false
    }
    
    // Handle different possible types
    switch flags := featureFlagsRaw.(type) {
    case []interface{}:
        for _, flag := range flags {
            if flagStr, ok := flag.(string); ok && flagStr == flagKey {
                return true
            }
        }
    case []string:
        for _, flag := range flags {
            if flag == flagKey {
                return true
            }
        }
    default:
        // Unexpected type, log warning
        log.Warn().Msgf("Unexpected feature_flags type: %T", flags)
    }
    
    return false
}

// Get user info from JWT with defensive parsing
func GetUserInfo(c *gin.Context) (UserInfo, error) {
    claims := getJWTClaims(c)
    
    userInfo := UserInfo{}
    
    // Parse sub (required)
    sub, ok := claims["sub"].(string)
    if !ok {
        return userInfo, fmt.Errorf("missing or invalid sub claim")
    }
    userInfo.ID = sub
    
    // Parse email (required)
    email, ok := claims["email"].(string)
    if !ok {
        return userInfo, fmt.Errorf("missing or invalid email claim")
    }
    userInfo.Email = email
    
    // Parse name (optional)
    if name, ok := claims["name"].(string); ok {
        userInfo.Name = name
    }
    
    // Parse email_verified (optional, defaults to false)
    if emailVerified, ok := claims["email_verified"].(bool); ok {
        userInfo.EmailVerified = emailVerified
    }
    
    // Parse groups with type checking
    if groupsRaw, ok := claims["groups"]; ok {
        switch groups := groupsRaw.(type) {
        case []interface{}:
            for _, g := range groups {
                if groupStr, ok := g.(string); ok {
                    userInfo.Groups = append(userInfo.Groups, groupStr)
                }
            }
        case []string:
            userInfo.Groups = groups
        }
    }
    
    // Check admin status
    userInfo.IsAdmin = hasAdminRole(claims)
    
    return userInfo, nil
}

### Feature Flag Storage

#### In Keycloak (Source of Truth)
Store as group attributes:
```json
{
  "name": "jan_group",
  "attributes": {
    "feature_flags": ["experimental_models"]
  }
}

In Jan Server Database (Definitions Only)

  • feature_flags table - Feature flag metadata (key, name, description, category)
  • audit_logs table - Audit trail for admin actions

In JWT (All Runtime Data)

{
  "sub": "user_id",
  "email": "[email protected]",
  "name": "John Doe",
  "preferred_username": "john",
  "email_verified": true,
  "groups": ["/jan_group", "/pilot_users"],
  "realm_access": {
    "roles": ["admin"]
  },
  "feature_flags": ["experimental_models"]
}

JWT contains everything needed - no database queries for user/group info

Available Feature Flags

# Model Access Control
experimental_models: Enable access to experimental/beta models in model_catalogs table
  - Description: Users with this flag can see models marked as experimental=true
  - Usage: Filter model catalog API based on this flag
  - Groups with access: jan_group, pilot_users
  - Default groups (standard, guest): No access to experimental models

Model Catalog Integration

Models in model_catalogs table have an experimental flag:

-- Example model catalog entries
INSERT INTO model_catalogs (id, model_id, name, experimental) VALUES
  ('1', 'gpt-4', 'GPT-4', false),  -- Stable, visible to all users
  ('2', 'gpt-4-turbo', 'GPT-4 Turbo', true),  -- Experimental, requires flag
  ('3', 'claude-3', 'Claude 3', false),  -- Stable, visible to all users
  ('4', 'jan-v3-experimental', 'Jan v3', true);  -- Experimental, requires flag

API filtering:

func GetModelCatalog(c *gin.Context) {
    userID := c.GetString("user_id")
    hasExperimentalAccess := featureFlagService.IsFeatureEnabled(userID, "experimental_models")
    
    query := "SELECT * FROM model_catalogs WHERE 1=1"
    if !hasExperimentalAccess {
        query += " AND experimental = false"
    }
    query += " ORDER BY name"
    
    // Execute query...
}

User Status

🟢 Active

  • User can log in and use the platform
  • All features accessible based on role and group feature flags
  • Default status for new users after verification
  • Keycloak Representation: enabled: true

⚪ Inactive

  • User account disabled
  • Cannot log in
  • Sessions invalidated by Keycloak
  • Reversible by admin
  • Keycloak Representation: enabled: false

🔵 Pending Verification

  • User registered but email not verified
  • Limited or no access until verification
  • Keycloak Representation: emailVerified: false

🔴 Banned

  • User permanently restricted from access
  • Cannot log in, requires admin intervention
  • Keycloak Representation: enabled: false with custom attribute banned: true

API Security - Admin-Only Endpoints

Protected Routes with RequireAdmin Middleware

All endpoints under /api/v1/admin/** require admin authentication:

adminRoutes := router.Group("/api/v1/admin")
adminRoutes.Use(middleware.RequireAdmin())
{
    // User Management
    adminRoutes.GET("/users", handlers.ListUsers)
    adminRoutes.POST("/users", handlers.CreateUser)
    adminRoutes.GET("/users/:id", handlers.GetUser)
    adminRoutes.PATCH("/users/:id", handlers.UpdateUser)
    adminRoutes.DELETE("/users/:id", handlers.DeleteUser)
    adminRoutes.POST("/users/:id/activate", handlers.ActivateUser)
    adminRoutes.POST("/users/:id/deactivate", handlers.DeactivateUser)
    adminRoutes.POST("/users/:id/roles/:roleName", handlers.AssignRole)
    adminRoutes.DELETE("/users/:id/roles/:roleName", handlers.RemoveRole)
    
    // Group Management
    adminRoutes.GET("/groups", handlers.ListGroups)
    adminRoutes.POST("/groups", handlers.CreateGroup)
    adminRoutes.GET("/groups/:id", handlers.GetGroup)
    adminRoutes.PATCH("/groups/:id", handlers.UpdateGroup)
    adminRoutes.DELETE("/groups/:id", handlers.DeleteGroup)
    adminRoutes.GET("/groups/:id/members", handlers.GetGroupMembers)
    adminRoutes.POST("/users/:userId/groups/:groupId", handlers.AddUserToGroup)
    adminRoutes.DELETE("/users/:userId/groups/:groupId", handlers.RemoveUserFromGroup)
    
    // Feature Flag Management (Group Level)
    adminRoutes.GET("/feature-flags", handlers.ListFeatureFlags)
    adminRoutes.POST("/feature-flags", handlers.CreateFeatureFlag)
    adminRoutes.PATCH("/feature-flags/:id", handlers.UpdateFeatureFlag)
    adminRoutes.DELETE("/feature-flags/:id", handlers.DeleteFeatureFlag)
    adminRoutes.GET("/groups/:id/feature-flags", handlers.GetGroupFeatureFlags)
    adminRoutes.PATCH("/groups/:id/feature-flags", handlers.SetGroupFeatureFlags)
    adminRoutes.POST("/groups/:id/feature-flags/:flagKey", handlers.EnableGroupFeatureFlag)
    adminRoutes.DELETE("/groups/:id/feature-flags/:flagKey", handlers.DisableGroupFeatureFlag)
    
    // System Monitoring
    adminRoutes.GET("/system/health", handlers.GetSystemHealth)
    adminRoutes.GET("/system/metrics", handlers.GetSystemMetrics)
    adminRoutes.GET("/audit-logs", handlers.GetAuditLogs)
}

RequireAdmin Middleware

// filepath: services/llm-api/internal/middleware/admin.go
package middleware

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

// Helper function to check admin status with defensive parsing
func isAdmin(claims map[string]interface{}) bool {
    // Check realm_access.roles for 'admin'
    realmAccessRaw, ok := claims["realm_access"]
    if ok {
        realmAccess, ok := realmAccessRaw.(map[string]interface{})
        if ok {
            rolesRaw, ok := realmAccess["roles"]
            if ok {
                // Handle both []interface{} and []string
                switch roles := rolesRaw.(type) {
                case []interface{}:
                    for _, role := range roles {
                        if roleStr, ok := role.(string); ok && roleStr == "admin" {
                            return true
                        }
                    }
                case []string:
                    for _, role := range roles {
                        if role == "admin" {
                            return true
                        }
                    }
                }
            }
        }
    }
    
    // Fallback: check is_admin attribute
    if attributes, ok := claims["attributes"].(map[string]interface{}); ok {
        if isAdminAttr, ok := attributes["is_admin"].(bool); ok && isAdminAttr {
            return true
        }
    }
    
    return false
}

func RequireAdmin() gin.HandlerFunc {
    return func(c *gin.Context) {
        claimsRaw, exists := c.Get("claims")
        if !exists {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
                "message": "Authentication required",
            })
            c.Abort()
            return
        }
        
        claims, ok := claimsRaw.(map[string]interface{})
        if !ok {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
                "message": "Invalid claims format",
            })
            c.Abort()
            return
        }
        
        // Verify required claims exist
        if _, ok := claims["sub"]; !ok {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
                "message": "Missing user identifier",
            })
            c.Abort()
            return
        }
        
        if _, ok := claims["email"]; !ok {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Unauthorized",
                "message": "Missing user email",
            })
            c.Abort()
            return
        }
        
        // Check admin status using helper
        if !isAdmin(claims) {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "Forbidden",
                "message": "Admin access required",
            })
            c.Abort()
            return
        }
        
        c.Next()
    }
}

Security Enhancements

  • Audit Logging: Log to PostgreSQL audit_logs table

    • All admin actions with full context (user_id, email, action, resource, payload)
    • Include IP address and user agent for security tracking
    • Retention: 90 days (configurable)
    • Indexed for fast querying by admin_user_id, resource_type, and created_at
  • Input Validation: Use github.com/go-playground/validator/v10

    • Validate email format, name length, etc.
    • Sanitize to prevent injection
    • Validate flag keys (alphanumeric + underscore)
  • CSRF Protection: Use Gin CSRF middleware

    • Token validation on all mutations
  • MFA: Enforce via Keycloak for admin users

Operational Guardrails

Rate Limiting

  • Implement rate limiting on admin endpoints
    • Use Kong rate limiting plugin or middleware
    • Limit: 100 requests/minute per admin user
    • Separate limits for read (higher) vs write (lower) operations
    • Return 429 Too Many Requests with Retry-After header

Request Logging

  • Structured logging for all admin endpoints
    • Log level: INFO for successful operations, WARN for 4xx, ERROR for 5xx
    • Include: timestamp, admin_user_id, admin_email, action, resource_type, resource_id, status_code, duration_ms
    • Use correlation ID for request tracing across services
    • Integrate with existing observability stack (Prometheus/Grafana)

Keycloak Resilience

  • Circuit breaker for Keycloak Admin API calls

    • Use github.com/sony/gobreaker (already in tech stack)
    • Threshold: 5 consecutive failures
    • Timeout: 30 seconds
    • Reset after: 60 seconds of successful calls
    • Fallback: Return 503 Service Unavailable with meaningful error
  • Retry logic with exponential backoff

    • Max retries: 3
    • Initial delay: 100ms
    • Max delay: 2 seconds
    • Only retry on network errors or 5xx responses
    • Do not retry on 4xx client errors
  • Monitoring and alerting

    • Alert on circuit breaker open state
    • Alert on Keycloak API latency > 1 second
    • Alert on Keycloak API error rate > 5%
    • Dashboard showing Keycloak health, request rate, error rate, latency percentiles

Implementation Tasks

Phase 0: Keycloak Integration

  • Install Dependencies

    go get github.com/Nerzal/gocloak/v13
    go get github.com/sony/gobreaker
    go get github.com/go-playground/validator/v10
  • Keycloak Client Setup

    • pkg/keycloak/client.go - Admin client wrapper with connection pooling
    • pkg/keycloak/users.go - User operations
    • pkg/keycloak/groups.go - Group operations
    • pkg/keycloak/roles.go - Role operations

Phase 1: Database Schema (Minimal)

  • Create Migration: services/llm-api/migrations/YYYYMMDD_user_management.sql

  • feature_flags table (definitions only):

    CREATE TABLE feature_flags (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        key VARCHAR(50) UNIQUE NOT NULL,
        name VARCHAR(255) NOT NULL,
        description TEXT,
        category VARCHAR(50),
        metadata JSONB,
        created_at TIMESTAMP DEFAULT NOW(),
        updated_at TIMESTAMP DEFAULT NOW()
    );
    CREATE INDEX idx_feature_flags_key ON feature_flags(key);
    CREATE INDEX idx_feature_flags_category ON feature_flags(category);
    
    -- Insert the experimental_models flag
    INSERT INTO feature_flags (key, name, description, category) VALUES
    ('experimental_models', 'Experimental Models', 'Access to experimental/beta models in model catalog', 'model_access');
  • audit_logs table:

    CREATE TABLE audit_logs (
        id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        admin_user_id VARCHAR(255) NOT NULL,  -- From JWT sub
        admin_email VARCHAR(255) NOT NULL,     -- From JWT email
        action VARCHAR(100) NOT NULL,
        resource_type VARCHAR(50) NOT NULL,
        resource_id VARCHAR(255),
        payload JSONB,
        ip_address VARCHAR(45),
        user_agent TEXT,
        status_code INTEGER,
        error_message TEXT,
        created_at TIMESTAMP DEFAULT NOW()
    );
    CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC);
    CREATE INDEX idx_audit_logs_admin_user_id ON audit_logs(admin_user_id);
    CREATE INDEX idx_audit_logs_resource_type ON audit_logs(resource_type);
  • Add experimental column to model_catalogs:

    ALTER TABLE model_catalogs ADD COLUMN IF NOT EXISTS experimental BOOLEAN DEFAULT FALSE;
    CREATE INDEX idx_model_catalogs_experimental ON model_catalogs(experimental);

Phase 2: JWT & Authentication

  • JWT Middleware Enhancement: services/llm-api/internal/middleware/auth.go

    // Extract and store JWT claims in context
    func JWTAuth() gin.HandlerFunc {
        return func(c *gin.Context) {
            // Validate JWT (already done by Kong or existing middleware)
            claims := extractJWTClaims(c)
            
            // Store in context for easy access
            c.Set("claims", claims)
            c.Set("user_id", claims["sub"])
            c.Set("user_email", claims["email"])
            c.Set("user_groups", claims["groups"])
            c.Set("feature_flags", claims["feature_flags"])
            
            c.Next()
        }
    }
  • RequireAdmin Middleware: Check JWT for admin role

    func RequireAdmin() gin.HandlerFunc {
        return func(c *gin.Context) {
            claims := c.MustGet("claims").(map[string]interface{})
            
            // Check admin status using helper function
            if !isAdmin(claims) {
                c.JSON(http.StatusForbidden, gin.H{
                    "error": "Forbidden",
                    "message": "Admin access required",
                })
                c.Abort()
                return
            }
            
            c.Next()
        }
    }

Phase 3: Model Catalog Filtering

  • Update Model Catalog Handler: services/llm-api/internal/handlers/model_catalog.go
    func GetModelCatalog(c *gin.Context) {
        // Check if user has experimental model access from JWT
        hasExperimentalAccess := IsFeatureEnabled(c, "experimental_models")
        
        query := "SELECT * FROM model_catalogs WHERE 1=1"
        if !hasExperimentalAccess {
            query += " AND (experimental = false OR experimental IS NULL)"
        }
        query += " ORDER BY name"
        
        // Execute query and return models
        rows, err := db.Query(query)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch models"})
            return
        }
        defer rows.Close()
        
        // Parse and return results...
    }

Phase 4: User Management API

  • Implement User Management Handlers: services/llm-api/internal/handlers/admin/users.go
    • ListUsers, CreateUser, GetUser, UpdateUser, DeleteUser
    • ActivateUser, DeactivateUser
    • All operations via Keycloak Admin API
    • Audit logging for all actions

Phase 5: Group Management API

  • Implement Group Management Handlers: services/llm-api/internal/handlers/admin/groups.go
    • ListGroups, CreateGroup, GetGroup, UpdateGroup, DeleteGroup
    • GetGroupMembers, AddUserToGroup, RemoveUserFromGroup
    • All operations via Keycloak Admin API
    • Audit logging for all actions

Phase 6: Feature Flag Management API

  • Implement Feature Flag Handlers: services/llm-api/internal/handlers/admin/feature_flags.go
    • ListFeatureFlags (from database), CreateFeatureFlag, UpdateFeatureFlag, DeleteFeatureFlag
    • GetGroupFeatureFlags (from Keycloak), SetGroupFeatureFlags
    • EnableGroupFeatureFlag, DisableGroupFeatureFlag
    • Update Keycloak group attributes
    • Audit logging for all flag changes

Phase 7: Keycloak JWT Mapper Configuration

  • Configure Group Mapper in Keycloak to include group paths in JWT

    • Mapper Type: Group Membership
    • Token Claim Name: groups
    • Full group path: Yes (includes leading slash: /jan_group, /pilot_users)
    • Add to ID token: Yes
    • Add to access token: Yes
    • Add to userinfo: Yes
  • Configure Group Attribute Mapper for feature flags

    • Mapper Type: Group Attribute Mapper (or User Attribute if aggregating)
    • Group Attribute: feature_flags
    • Token Claim Name: feature_flags
    • Claim JSON Type: JSON (array of strings)
    • Aggregate attribute values: Yes (combine from all groups)
    • Add to ID token: Yes
    • Add to access token: Yes
  • Note on Group Paths:

    • Keycloak includes leading slash in group paths: /jan_group, /standard
    • Nested groups use hierarchy: /parent/child
    • Strip leading slash in application code if needed for comparisons
    • Group inheritance: Users in child groups automatically in parent groups
  • Example JWT with all needed data:

    {
      "sub": "user-uuid",
      "email": "[email protected]",
      "name": "John Doe",
      "preferred_username": "john",
      "email_verified": true,
      "groups": ["/jan_group", "/standard"],
      "realm_access": {
        "roles": ["admin", "user"]
      },
      "feature_flags": ["experimental_models"]
    }

Phase 8: Testing & Validation

  • Unit Tests: Test feature flag resolution, admin middleware, JWT parsing
  • Integration Tests: Test model catalog filtering with different user groups
  • End-to-End Tests: Test admin operations with Keycloak
  • Performance Tests: Validate JWT-only approach performance

Database Queries

Get Users with Experimental Model Access (from audit logs)

-- This is for audit/reporting only, not for runtime checks
SELECT DISTINCT admin_email, action, created_at
FROM audit_logs
WHERE resource_type = 'group_feature_flag'
AND payload->>'flag_key' = 'experimental_models'
ORDER BY created_at DESC;

Get User's Effective Feature Flags (from JWT)

// No database query needed - read from JWT claims
func GetUserFeatures(c *gin.Context) []string {
    if featureFlags, exists := c.Get("feature_flags"); exists {
        if flags, ok := featureFlags.([]string); ok {
            return flags
        }
    }
    return []string{}
}

Get Experimental Models

-- Get all experimental models (admin view)
SELECT * FROM model_catalogs 
WHERE experimental = true
ORDER BY name;

-- Get models available to user without experimental access
SELECT * FROM model_catalogs 
WHERE experimental = false OR experimental IS NULL
ORDER BY name;

-- Get all models (for users with experimental_models flag)
SELECT * FROM model_catalogs
ORDER BY name;

Feature Flag Audit Trail

-- See who changed feature flags for a group
SELECT 
  admin_email,
  action,
  payload->>'group_id' as group_id,
  payload->>'group_name' as group_name,
  payload->>'flag_key' as flag_key,
  payload->>'enabled' as enabled,
  created_at
FROM audit_logs
WHERE resource_type = 'group_feature_flag'
ORDER BY created_at DESC
LIMIT 100;

Future Enhancements

  • User impersonation (via Keycloak Admin API)

    • Allow admins to impersonate users for troubleshooting
    • Audit log all impersonation sessions
  • Bulk operations

    • Bulk user import/export
    • Bulk group membership changes
    • Bulk feature flag assignments
  • Additional feature flags as product evolves

    • fine_tuning: Access to model fine-tuning features
    • custom_models: Upload and use custom models
    • advanced_analytics: Access to advanced usage analytics
    • api_access: Programmatic API access with keys
  • Advanced feature flag capabilities

    • A/B testing with user percentage rollout
    • Gradual rollout with canary deployment
    • Time-based scheduling (enable/disable at specific times)
    • Environment-specific flags (dev/staging/prod)

Frontend Implementation Scope (Future)

Note: Frontend implementation is out of scope for this backend-focused document. If a UI is needed, it should:

  • Admin Dashboard (separate project)

    • User management interface (list, create, edit, deactivate users)
    • Group management interface (list, create, edit, manage members)
    • Feature flag management UI (assign flags to groups)
    • Audit log viewer with filtering and export
  • Tech Stack Recommendations

    • Framework: React or Next.js
    • UI Library: shadcn/ui or Material-UI
    • State Management: React Query (for API cache management)
    • Tables: TanStack Table
    • Forms: React Hook Form + Zod validation
  • Authentication

    • Use Keycloak's JavaScript adapter for SSO
    • Redirect to Keycloak login
    • Store JWT in httpOnly cookie or memory
    • Handle token refresh automatically

For now, admin operations can be performed via:

  • Direct Keycloak Admin Console
  • API testing tools (Postman, Insomnia, curl)
  • Custom CLI tools if needed

Architecture Summary

Key Principles

  • JWT-Only: All user/group/feature flag data comes from JWT claims - no server-side cache or sync
  • Keycloak as Source of Truth: Users, groups, roles, and feature flag assignments managed in Keycloak
  • Minimal Database: Only audit logs and feature flag definitions (metadata) stored in PostgreSQL
  • Real-Time Admin Operations: Admin API calls directly to Keycloak for immediate effect
  • Automatic JWT Updates: Feature flag changes reflected in next JWT after token refresh (handled by Keycloak)