A comprehensive PostgreSQL database model for multi-tenant user/group/permissions management with hierarchical permissions, permission sets, and flexible identity provider integration.
This is a standalone PostgreSQL framework that provides complete tenant/user/group/permissions management for any PostgreSQL project. It's production-ready and used in many real-world projects, both big and small.
- Multi-tenancy with isolated permissions and data
- Hierarchical permissions using PostgreSQL ltree (e.g.,
users.create_user.admin_level) - Flexible group types: Internal, External (mapped to identity providers), and Hybrid
- Identity provider integration: Works with any provider (Windows Auth, AzureAD, Google, Facebook, KeyCloak, LDAP, etc.)
- Permission sets for role-based access control
- Resource-level ACL for per-resource access control (grants, denies, group inheritance)
- Language & translation with full-text and accent-insensitive search
- API key management with technical user pattern
- Comprehensive audit logging with multi-key journal entries and event categories
- App bootstrapping with idempotent ensure functions and optional
_is_final_statedeclarative sync - Auto-lockout on repeated login failures with configurable threshold and time window
- Multi-factor authentication (TOTP) with two-step enrollment, recovery codes, challenge/verify flow, recovery reset, and policy-based enforcement
- Invitation system with phases, conditions, templates, payload schema resolution, and backend/external action orchestration
- User blacklist for blocking re-creation of deleted/banned users (username + provider identity)
- Permission caching for performance with automatic invalidation
- Real-time notifications via PostgreSQL LISTEN/NOTIFY for permission changes
- Built-in search/paging for users, groups, tenants, permissions, and permission sets
# Full database setup (recreate, restore, and update)
./debee.ps1 -Operations fullService
# Or individual operations
./debee.ps1 -Operations recreateDatabase
./debee.ps1 -Operations updateDatabaseConfigure connection in debee.env:
PGHOST=localhost
PGPORT=5432
PGUSER=postgres
PGPASSWORD=your_password
DBDESTDB=your_database_name
Use debee's execSql operation:
./debee.ps1 -Operations execSql -Sql "SELECT * FROM auth.user_info;" # Inline SQL
./debee.ps1 -Operations execSql -SqlFile script.sql # Run file
./debee.ps1 -Operations execSql # Interactive psqlmake test # Run all tests
make test FILTER=resource # Run filtered subset
./debee.ps1 -Operations runTests # Direct debee call
./debee.ps1 -Operations runTests -TestFilter provider # Filtered-- Check user permission (throws exception if denied)
perform auth.has_permission(_tenant_id, _user_id, 'orders.cancel_order');
-- Silent permission check
if auth.has_permission(_tenant_id, _user_id, 'orders.view', _throw_err := false) then
-- user has permission
end if;
-- Create permission
select auth.create_permission_by_path('orders.cancel_order', 'Cancel Order');
-- Assign permission to user
select auth.assign_permission(_created_by, _user_id, _tenant_id, _target_user_id, null, 'orders.cancel_order');
-- Search users with pagination
select * from auth.search_users(
_user_id := 1,
_search_text := 'john',
_is_active := true,
_page := 1,
_page_size := 20,
_tenant_id := 1
);
-- Search groups with member counts
select * from auth.search_user_groups(1, 'admin', _is_active := true);
-- Search permissions by parent
select * from auth.search_permissions(1, null, _parent_code := 'users');Idempotent bulk-create functions for application startup. Call them every boot — they create what's missing, skip what exists, and optionally remove what's no longer defined.
-- Define permissions (safe to call every startup)
select * from auth.ensure_permissions('app', 1, null, '[
{"title": "Documents", "is_assignable": false},
{"title": "Read documents", "parent_code": "documents"},
{"title": "Write documents", "parent_code": "documents"}
]', 'my_app');
-- Define permission sets with their permissions
select * from auth.ensure_perm_sets('app', 1, null, '[
{"title": "Document Viewer", "permissions": ["documents.read_documents"]},
{"title": "Document Editor", "permissions": ["documents.read_documents", "documents.write_documents"]}
]', 'my_app');
-- Define groups
select * from auth.ensure_user_groups('app', 1, null, '[
{"title": "Document Editors"},
{"title": "Document Viewers", "is_external": true}
]', 1, 'my_app');
-- Define group mappings (by title or ID)
select * from auth.ensure_user_group_mappings('app', 1, null, '[
{"user_group_title": "Document Viewers", "provider_code": "azure_ad",
"mapped_object_id": "aad-group-guid", "mapped_object_name": "Corp Viewers"}
]');Final state sync — pass _is_final_state := true to make the input the complete definition. Items with the same _source not in the input are removed:
-- Remove "Write documents" on next deploy — just omit it
select * from auth.ensure_permissions('app', 1, null, '[
{"title": "Documents", "is_assignable": false},
{"title": "Read documents", "parent_code": "documents"}
]', 'my_app', _is_final_state := true);The _source parameter scopes deletions — each module manages only its own items without affecting others.
auth.user_info- Core user dataauth.user_identity- Multiple identity provider support per userauth.user_data- Extensible custom user fieldsauth.tenant- Multi-tenancy managementauth.user_group- Three group types (internal/external/hybrid)auth.permission- Global hierarchical permissionsauth.perm_set- Tenant-specific permission collectionsauth.resource_access- Resource-level ACL (list-partitioned by resource_type)auth.user_mfa- MFA enrollment state, encrypted secrets, hashed recovery codesauth.mfa_policy- MFA enforcement rules (scope: user > group > tenant > global)auth.invitation- Invitation lifecycle with phases, conditions, and action orchestrationauth.invitation_template- Reusable invitation templates with action definitionsauth.user_blacklist- Blacklist for deleted/banned user identitiesauth.api_key- Service authentication with technical usersconst.language/public.translation- Language registry and translation storage with full-text searchpublic.journal- Audit logging with multi-key support, request context tracking (range-partitioned by month)auth.user_event- Security event audit trail with request context tracking (range-partitioned by month)
public&auth- Always validate permissionsinternal- For trusted contexts (no permission checks)unsecure- Security system internals only
The system ships with dedicated service accounts (IDs 1-999 reserved) so backends never need to use the system superuser (user_id:1) at runtime. Each account has only the permissions required for its job.
| ID | Username | Purpose |
|---|---|---|
| 1 | system |
Seed/migration — has has_permissions bypass, should not be used at runtime |
| 2 | svc_registrator |
User registration + email/phone verification token creation |
| 3 | svc_authenticator |
Login, permission resolution, token validation |
| 4 | svc_token_manager |
Full token lifecycle (password reset, email verification, etc.) |
| 5 | svc_api_gateway |
API key validation at gateway/middleware level |
| 6 | svc_group_syncer |
Background external group member synchronization |
| 800 | svc_data_processor |
Generic app-level processing (empty perm set — app adds its own) |
All service accounts have user_type_code = 'service', can_login = false, is_system = true.
Usage: pass the appropriate service account's user_id instead of 1 when calling auth.* functions from your backend:
-- Before (superuser bypass — no permission check at all)
select auth.has_permission(1, null, 'users.register_user', 1);
-- After (least-privilege — actually validates the permission)
select auth.has_permission(2, null, 'users.register_user', 1);The svc_data_processor (ID 800) is the recommended default for application-specific operations. Add permissions to its svc_data_processor_permissions perm set as needed.
Composable permission sets for human administrators. Each set covers a specific administrative domain — assign one or combine several for composite roles.
| Permission Set | Domain | Key Permissions |
|---|---|---|
User manager |
User CRUD + audit | users, authentication.read_user_events |
Group manager |
Groups & membership | groups |
Permission manager |
Permissions & perm sets | permissions |
Provider manager |
Identity providers | providers |
Token manager |
Token lifecycle & config | tokens.*, token_configuration |
Api key manager |
API key CRUD | api_keys |
Auditor |
Read-only audit access | journal, authentication.read_user_events, read-only entity access |
Full admin |
Everything combined | All admin permissions including journal.purge_journal |
All sets include journal.read_journal and journal.get_payload for audit visibility (except Auditor which has the full journal parent permission).
Built-in group: "Full admins" (group ID 3) has the full_admin perm set assigned. Add users to this group for complete administrative access:
-- Add user to Full admins group
select auth.create_user_group_member('admin', 1, null, 3, _target_user_id := 1001);
-- Or assign individual role perm sets to a user
select auth.assign_permission('admin', 1, null, 1, null, 1001, null, 'user_manager');
select auth.assign_permission('admin', 1, null, 1, null, 1001, null, 'group_manager');The public.journal table provides comprehensive audit logging with multi-key support. The journal is shared across all modules — the permissions system logs its own events here, and applications can register custom event codes and create their own journal entries alongside the core ones.
-- Using event code name (recommended)
select * from create_journal_message(
'admin', -- created_by
1, -- user_id
'user_created', -- event code (text)
'New user registered',
_keys := '{"user": 123, "tenant": 1}'::jsonb
);
-- Single entity convenience
select * from create_journal_message(
'admin', 1, 'group_created', 'Group created',
'group', 456 -- entity_type, entity_id
);
-- With request context (stored in journal.request_context column)
select * from create_journal_message_for_entity(
'admin', 1, 'corr-123',
10001, 'user', 123,
'{"username": "john"}'::jsonb,
1,
_request_context := '{"ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0"}'::jsonb
);-- Search by event category (for UI filtering like "show all user events")
SELECT * FROM search_journal(
_user_id := 1,
_event_category := 'user_event'
);
-- Search by entity keys
SELECT * FROM search_journal(
_user_id := 1,
_keys_criteria := '{"order": 3}'::jsonb
);
-- Full-text search with filters
SELECT * FROM search_journal(
_user_id := 1,
_search_text := 'password',
_event_category := 'user_event',
_from := now() - interval '7 days'
);| Category | Description |
|---|---|
user_event |
User lifecycle, login, password changes |
tenant_event |
Tenant management |
permission_event |
Permission assignments |
group_event |
Group membership changes |
apikey_event |
API key operations |
token_event |
Token lifecycle |
provider_event |
Provider lifecycle |
maintenance_event |
System maintenance operations |
resource_event |
Resource access (ACL) operations |
language_event |
Language CRUD |
translation_event |
Translation CRUD and copy operations |
invitation_event |
Invitation lifecycle, actions, and templates |
Higher-level query functions for common audit needs:
-- Unified audit trail for a specific user (combines journal + user_event)
select * from auth.get_user_audit_trail(
_user_id := 1,
_target_user_id := 1001,
_from := now() - interval '30 days',
_page := 1,
_page_size := 20
);
-- Security events across the system (failed logins, lockouts, permission denials)
select * from auth.get_security_events(
_user_id := 1,
_from := now() - interval '7 days'
);All security-relevant functions (user management, token operations, API key validation) accept an optional _request_context jsonb parameter. This stores caller metadata (IP address, user agent, origin, device ID, etc.) alongside the event and journal records without requiring schema changes when new fields are needed.
-- Pass request context to any security function
select auth.enable_user('admin', 1, 'corr-123', 42,
_request_context := jsonb_build_object(
'ip_address', '192.168.1.1',
'user_agent', 'Mozilla/5.0',
'origin', 'https://app.example.com',
'device_id', 'abc-123'
));
-- Context is stored on auth.user_event.request_context, auth.token.request_context,
-- and public.journal.request_context — queryable with standard jsonb operators
select request_context ->> 'ip_address' as ip
from auth.user_event
where target_user_id = 42;Both public.journal and auth.user_event are range-partitioned by created_at (monthly). This enables:
- Partition pruning — date-filtered queries only scan relevant monthly partitions
- Instant purge — old partitions are detached and dropped instead of row-by-row
DELETE - INSERT performance — writes target the current month's partition (smaller indexes)
Configuration in const.sys_param (see System Parameters for the full reference):
-- Purge data older than configured retention (requires journal.purge_journal permission)
-- Drops old partitions + cleans default partition + pre-creates future partitions
select * from public.purge_audit_data('admin', 1, null);
-- Purge with explicit retention override
select * from public.purge_audit_data('admin', 1, null, _older_than_days := 90);
-- Pre-create future partitions manually (also called automatically by purge)
select unsecure.ensure_audit_partitions(3);The purge itself is journaled (event 17001 audit_data_purged) for accountability.
For large deployments where storing all journal/event data in PostgreSQL is unnecessary, the system supports offloading data to an external store (e.g., ClickHouse) via PostgreSQL's LISTEN/NOTIFY. Storage mode is controlled independently for journal and user events:
| Mode | Behavior |
|---|---|
local |
INSERT into PostgreSQL only (default — no behavior change) |
notify |
Fire pg_notify only, skip INSERT — an external listener captures the data |
both |
INSERT into PostgreSQL AND fire pg_notify |
-- Switch journal to notify-only (stop storing in PostgreSQL)
select auth.update_sys_param(1, 'journal', 'storage_mode', 'notify');
-- Switch user events to both (store locally + notify external)
select auth.update_sys_param(1, 'user_event', 'storage_mode', 'both');
-- Read current storage mode
select (auth.get_sys_param('journal', 'storage_mode')).text_value;Backend listener setup:
// Dedicated connection (not from pool — PgBouncer transaction mode doesn't support LISTEN)
await client.query('LISTEN journal_events');
await client.query('LISTEN user_events');
client.on('notification', (msg) => {
const event = JSON.parse(msg.payload);
if (event.truncated) {
// Large fields were stripped — handle accordingly
}
clickhouse.insert(msg.channel, event);
});Notify channels: journal_events and user_events (separate from permission_changes)
Payload truncation: pg_notify has an 8000 byte limit. If a payload exceeds ~7900 bytes, large fields (data_payload/request_context for journal, event_data/request_context for user events) are stripped and "truncated": true is added.
Note: When mode is notify, search/query functions (search_journal, get_journal_entry, search_user_events, etc.) return empty results since data is not stored in PostgreSQL. The application should query the external store directly for audit data.
Runtime configuration is stored in const.sys_param and managed via auth.get_sys_param() / auth.update_sys_param(). The setter is restricted to user_id = 1 (system user) — intended for app startup.
-- Read a parameter
select (auth.get_sys_param('journal', 'level')).text_value;
-- Update a parameter (system user only)
select auth.update_sys_param(1, 'journal', 'level', 'all');| group_code | code | default | type | description |
|---|---|---|---|---|
journal |
level |
update |
text | Journal logging verbosity. all = log everything including reads, update = state-changing operations only, none = disable journaling |
journal |
retention_days |
365 |
text (cast to int) | How many days of journal entries to keep. Used by purge_audit_data() |
journal |
storage_mode |
local |
text | Where journal data goes. local = INSERT only, notify = pg_notify only, both = INSERT + pg_notify |
user_event |
retention_days |
365 |
text (cast to int) | How many days of user events to keep. Used by purge_audit_data() |
user_event |
storage_mode |
local |
text | Where user event data goes. Same modes as journal |
partition |
months_ahead |
3 |
number | How many future monthly partitions to pre-create for journal and user_event tables |
login_lockout |
max_failed_attempts |
5 |
number | Number of failed login attempts before auto-lock |
login_lockout |
window_minutes |
15 |
number | Time window in minutes for counting failed login attempts |
auth |
perm_cache_timeout_in_s |
300 (fallback) |
number | Permission cache TTL in seconds. Not seeded — uses hardcoded fallback if missing |
The system uses PostgreSQL's built-in LISTEN/NOTIFY to push permission change events to backends in real-time. When any permission-relevant mutation occurs (assignment, group membership, user status, etc.), a JSON notification is sent on the permission_changes channel.
// Dedicated connection (not from pool — PgBouncer transaction mode doesn't support LISTEN)
await client.query('LISTEN permission_changes');
client.on('notification', (msg) => {
const payload = JSON.parse(msg.payload);
// payload: { event, tenant_id, target_type, target_id, detail, at }
broadcastToAffectedClients(payload, { type: 'REFETCH_PERMISSIONS' });
});| Event | Trigger | target_type |
|---|---|---|
permission_assigned / permission_unassigned |
Permission or perm_set assigned to user/group | user or group |
perm_set_permissions_added / perm_set_permissions_removed |
Permissions added/removed from a set | perm_set |
group_member_added / group_member_removed |
User added/removed from group | user |
group_disabled / group_enabled / group_deleted |
Group status change | group |
group_mapping_created / group_mapping_deleted |
External group mapping change | group |
user_disabled / user_locked / user_deleted |
User status change | user |
owner_created / owner_deleted |
Ownership change | user |
provider_disabled / provider_deleted |
Identity provider change | provider |
tenant_deleted |
Tenant removal | tenant |
Notifications carry IDs, not user lists. After receiving a notification, query the matching resolution view:
-- Group event → which users to notify?
SELECT user_id FROM auth.notify_group_users WHERE user_group_id = $1;
-- Perm set changed → which users to notify?
SELECT user_id FROM auth.notify_perm_set_users WHERE perm_set_id = $1;
-- Permission assignability changed → which users to notify?
SELECT user_id FROM auth.notify_permission_users WHERE permission_id = $1;
-- Provider disabled/deleted → which users to notify?
SELECT user_id FROM auth.notify_provider_users WHERE provider_code = $1;
-- Tenant deleted → which users to notify?
SELECT user_id FROM auth.notify_tenant_users WHERE tenant_id = $1;For target_type = 'user' events (user status changes, group member add/remove, owner changes), the target_id is the user_id directly — no view needed.
Notifications are delivered after COMMIT (never for rolled-back transactions) and are fire-and-forget — cache invalidation handles correctness, notifications handle client freshness.
Supports three group types for flexible identity provider integration:
- Internal Groups: Traditional membership stored in database
- External Groups: Membership determined by identity provider mappings (no local storage)
- Hybrid Groups: Combination of both approaches
Example: User logs in with AzureAD groups → System maps external groups to internal permissions → User gets permissions without being explicitly added as member.
Providers represent external authentication systems (AzureAD, Google, LDAP, email, etc.). Each provider has a code, name, and is_active flag.
-- Create provider (requires providers.create_provider permission)
select * from auth.create_provider('admin', 1, null, 'google', 'Google authentication');
-- Idempotent: create if missing, return existing if found
select * from auth.ensure_provider('admin', 1, null, 'google', 'Google authentication');
-- Returns: __provider_id, __is_new (true on first call, false on subsequent)
-- Enable/disable
select * from auth.enable_provider('admin', 1, null, 'google');
select * from auth.disable_provider('admin', 1, null, 'google');- CLAUDE.md - Developer guide for Claude Code
- resource-access.md - Resource-level ACL system documentation
- CHANGELOG.md - Version history
- Complete Documentation - postgresql-permissions-model-docs (comprehensive documentation project)
- functions.md - Function reference
- features.md - Feature list
keen-auth-permissions— Elixir library with application-level wrappers around these SQL functions- C# library — planned
Always use fully qualified schema names to avoid search_path issues:
-- Good
select auth.has_permission(_tenant_id, _user_id, 'permission.code');
-- Avoid (can cause "cannot find function" errors)
select has_permission(_tenant_id, _user_id, 'permission.code');Simple version tracking with public.__version table:
select * from public.start_version_update('1.0', 'Initial version');
-- migration content
select * from public.stop_version_update('1.0');- PostgreSQL with extensions:
ltree,uuid-ossp,unaccent,pg_trgm - PowerShell (for setup scripts)
The system uses structured event/error codes organized by category. Codes are stored in const.event_code table.
| Range | Category | Description |
|---|---|---|
| 10001-10999 | User Events | Login, logout, password, identity management |
| 11001-11999 | Tenant Events | Tenant lifecycle and user access |
| 12001-12999 | Permission Events | Permission and permission set management |
| 13001-13999 | Group Events | Group membership and mappings |
| 14001-14999 | API Key Events | API key lifecycle and validation |
| 15001-15999 | Token Events | Token lifecycle |
| 16001-16999 | Provider Events | Provider lifecycle |
| 17001-17999 | Maintenance Events | System maintenance operations |
| 18001-18999 | Resource Access Events | Resource type and ACL operations |
| 19001-19999 | Token Config Events | Token configuration changes |
| 20001-20999 | Language Events | Language CRUD |
| 21001-21999 | Translation Events | Translation CRUD and copy operations |
| 22001-22999 | Invitation Events | Invitation lifecycle, actions, and templates |
| 30001-30999 | Security Errors | Authentication and token errors |
| 31001-31999 | Validation Errors | Missing required parameters |
| 32001-32999 | Permission Errors | Permission not found, not assignable |
| 33001-33999 | User/Group Errors | Entity not found, not active, system entity |
| 34001-34999 | Tenant Errors | Tenant access errors |
| 35001-35999 | Resource Access Errors | Resource ACL errors |
| 36001-36999 | Token Config Errors | Token configuration errors |
| 37001-37999 | Language/Translation Errors | Language and translation errors |
| 38001-38999 | MFA Errors | Multi-factor authentication errors |
| 39001-39999 | Invitation Errors | Invitation lifecycle errors |
| 50000+ | Reserved | Application-specific events and errors |
| Code | Event | Description |
|---|---|---|
| 10001 | user_created | New user account was created |
| 10002 | user_updated | User account was updated |
| 10003 | user_deleted | User account was deleted |
| 10004 | user_enabled | User account was enabled |
| 10005 | user_disabled | User account was disabled |
| 10006 | user_locked | User account was locked |
| 10007 | user_unlocked | User account was unlocked |
| 10010 | user_logged_in | User successfully logged in |
| 10011 | user_logged_out | User logged out |
| 10012 | user_login_failed | User login attempt failed |
| 10020 | password_changed | User password was changed |
| 10021 | password_reset_requested | Password reset was requested |
| 10022 | password_reset_completed | Password reset was completed |
| 10030 | identity_created | User identity was created |
| 10031 | identity_updated | User identity was updated |
| 10032 | identity_deleted | User identity was deleted |
| 10033 | identity_enabled | User identity was enabled |
| 10034 | identity_disabled | User identity was disabled |
| 10040 | email_verified | User email was verified |
| 10041 | phone_verified | User phone was verified |
| 10050 | mfa_enabled | Multi-factor authentication was enabled |
| 10051 | mfa_disabled | Multi-factor authentication was disabled |
| 10070 | external_data_updated | User data was updated from external source |
| 10080 | user_blacklisted | User was added to blacklist |
| 10081 | user_unblacklisted | User was removed from blacklist |
| 10082 | user_creation_blocked | User creation was blocked by blacklist |
| 10083 | user_auto_locked | User auto-locked after too many failed login attempts |
| 10090 | mfa_enrolled | MFA enrollment was initiated |
| 10091 | mfa_enrollment_confirmed | MFA enrollment was confirmed with a valid code |
| 10092 | mfa_challenge_created | MFA challenge token was created |
| 10093 | mfa_challenge_passed | MFA challenge was successfully verified |
| 10094 | mfa_recovery_used | MFA recovery code was used to pass challenge |
| 10095 | mfa_policy_created | MFA policy rule was created |
| 10096 | mfa_policy_deleted | MFA policy rule was deleted |
| 10097 | mfa_recovery_reset | MFA recovery codes were regenerated |
| Code | Event | Description |
|---|---|---|
| 11001 | tenant_created | New tenant was created |
| 11002 | tenant_updated | Tenant was updated |
| 11003 | tenant_deleted | Tenant was deleted |
| 11010 | tenant_user_added | User was added to tenant |
| 11011 | tenant_user_removed | User was removed from tenant |
| Code | Event | Description |
|---|---|---|
| 12001 | permission_created | New permission was created |
| 12002 | permission_updated | Permission was updated |
| 12003 | permission_deleted | Permission was deleted |
| 12010 | permission_assigned | Permission was assigned |
| 12011 | permission_revoked | Permission was revoked |
| 12020 | perm_set_created | New permission set was created |
| 12021 | perm_set_updated | Permission set was updated |
| 12022 | perm_set_deleted | Permission set was deleted |
| 12023 | perm_set_assigned | Permission set was assigned |
| 12024 | perm_set_revoked | Permission set was revoked |
| Code | Event | Description |
|---|---|---|
| 13001 | group_created | New group was created |
| 13002 | group_updated | Group was updated |
| 13003 | group_deleted | Group was deleted |
| 13010 | group_member_added | Member was added to group |
| 13011 | group_member_removed | Member was removed from group |
| 13020 | group_mapping_created | Group mapping was created |
| 13021 | group_mapping_deleted | Group mapping was deleted |
| 13030 | group_members_synced | External group members synchronized from provider |
| Code | Event | Description |
|---|---|---|
| 14001 | apikey_created | New API key was created |
| 14002 | apikey_updated | API key was updated |
| 14003 | apikey_deleted | API key was deleted |
| 14010 | apikey_validated | API key was validated |
| 14011 | apikey_validation_failed | API key validation failed |
| Code | Event | Description |
|---|---|---|
| 15001 | token_created | New token was created |
| 15002 | token_used | Token was used |
| 15003 | token_expired | Token expired |
| 15004 | token_failed | Token validation failed |
| Code | Event | Description |
|---|---|---|
| 16001 | provider_created | New provider was created |
| 16002 | provider_updated | Provider was updated |
| 16003 | provider_deleted | Provider was deleted |
| 16004 | provider_enabled | Provider was enabled |
| 16005 | provider_disabled | Provider was disabled |
| Code | Event | Description |
|---|---|---|
| 17001 | audit_data_purged | Old audit data was purged |
| Code | Event | Description |
|---|---|---|
| 18001 | resource_type_created | New resource type was registered |
| 18010 | resource_access_granted | Resource access was granted |
| 18011 | resource_access_revoked | Resource access was revoked |
| 18012 | resource_access_denied | Resource access was denied |
| 18013 | resource_access_bulk_revoked | All resource access was revoked for a resource |
| Code | Event | Description |
|---|---|---|
| 20001 | language_created | New language was created |
| 20002 | language_updated | Language was updated |
| 20003 | language_deleted | Language was deleted |
| Code | Event | Description |
|---|---|---|
| 21001 | translation_created | New translation was created |
| 21002 | translation_updated | Translation was updated |
| 21003 | translation_deleted | Translation was deleted |
| 21004 | translations_copied | Translations were copied between languages |
| Code | Event | Description |
|---|---|---|
| 22001 | invitation_created | New invitation was created |
| 22002 | invitation_accepted | Invitation was accepted by the recipient |
| 22003 | invitation_rejected | Invitation was rejected by the recipient |
| 22004 | invitation_revoked | Invitation was revoked by the inviter |
| 22005 | invitation_expired | Invitation expired without response |
| 22006 | invitation_action_completed | An invitation action was completed |
| 22007 | invitation_action_failed | An invitation action failed |
| 22008 | invitation_completed | All invitation actions were completed |
| 22009 | invitation_failed | Invitation processing failed due to a required action failure |
| 22010 | invitation_template_created | Invitation template was created |
| 22011 | invitation_template_updated | Invitation template was updated |
| 22012 | invitation_template_deleted | Invitation template was deleted |
| Code | Function | Description |
|---|---|---|
| 30001 | error.raise_30001 | API key/secret combination is not valid |
| 30002 | error.raise_30002 | Token is not valid or has expired |
| 30003 | error.raise_30003 | Token was created for different user |
| 30004 | error.raise_30004 | Token has already been used |
| 30005 | error.raise_30005 | Token does not exist |
| Code | Function | Description |
|---|---|---|
| 31001 | error.raise_31001 | Either user group or target user id must not be null |
| 31002 | error.raise_31002 | Either permission set code or permission code must not be null |
| 31003 | error.raise_31003 | Either permission id or code must not be null |
| 31004 | error.raise_31004 | Either mapped object id or mapped role must not be empty |
| Code | Function | Description |
|---|---|---|
| 32001 | error.raise_32001 | User does not have required permission |
| 32002 | error.raise_32002 | Permission does not exist |
| 32003 | error.raise_32003 | Permission is not assignable |
| 32004 | error.raise_32004 | Permission set does not exist |
| 32005 | error.raise_32005 | Permission set is not assignable |
| 32006 | error.raise_32006 | Permission set is not defined in this tenant |
| 32007 | error.raise_32007 | Parent permission does not exist |
| 32008 | error.raise_32008 | Some permissions are not assignable |
| Code | Function | Description |
|---|---|---|
| 33001 | error.raise_33001 | User does not exist |
| 33002 | error.raise_33002 | User is a system user |
| 33003 | error.raise_33003 | User is not in active state |
| 33004 | error.raise_33004 | User is locked out |
| 33005 | error.raise_33005 | User is not supposed to log in |
| 33006 | error.raise_33006 | User cannot be ensured for email provider |
| 33007 | error.raise_33007 | User identity is already in use |
| 33008 | error.raise_33008 | User identity is not in active state |
| 33009 | error.raise_33009 | User identity does not exist |
| 33010 | error.raise_33010 | Provider is not in active state |
| 33011 | error.raise_33011 | User group does not exist |
| 33012 | error.raise_33012 | User group is not active |
| 33013 | error.raise_33013 | User group is not assignable or is external |
| 33014 | error.raise_33014 | User group is a system group |
| 33015 | error.raise_33015 | User is not tenant or group owner |
| 33018 | error.raise_33018 | User is blacklisted and cannot be created |
| 33019 | error.raise_33019 | User identity is blacklisted and cannot be created |
| Code | Function | Description |
|---|---|---|
| 34001 | error.raise_34001 | User has no access to this tenant |
| Code | Function | Description |
|---|---|---|
| 35001 | error.raise_35001 | User has no access to resource |
| 35002 | error.raise_35002 | Neither user_id nor user_group_id provided |
| 35003 | error.raise_35003 | Resource type not found or inactive |
| 35004 | error.raise_35004 | Access flag not found |
| Code | Function | Description |
|---|---|---|
| 37001 | error.raise_37001 | Language does not exist |
| 37002 | error.raise_37002 | Translation does not exist |
| Code | Function | Description |
|---|---|---|
| 38001 | error.raise_38001 | MFA is already enrolled and confirmed for this type |
| 38002 | error.raise_38002 | MFA is not enrolled for this type |
| 38003 | error.raise_38003 | MFA enrollment is not confirmed |
| 38004 | error.raise_38004 | The provided MFA code is not valid |
| 38005 | error.raise_38005 | MFA verification is required |
| 38006 | error.raise_38006 | MFA type does not exist or is inactive |
| 38007 | error.raise_38007 | MFA policy does not exist |
| Code | Function | Description |
|---|---|---|
| 39001 | error.raise_39001 | Invitation does not exist |
| 39002 | error.raise_39002 | Invitation is not in pending state |
| 39003 | error.raise_39003 | Invitation has expired |
| 39004 | error.raise_39004 | Invitation action does not exist |
| 39005 | error.raise_39005 | Invitation action is not in pending or processing state |
| 39006 | error.raise_39006 | Invitation template does not exist or is inactive |
Old v1 error codes (52xxx) are still supported via aliases. See 016_functions_error.sql for the complete mapping.