Service Accounts & Admin Roles¶
The PostgreSQL Permissions Model ships with dedicated service accounts and composable admin permission sets so that applications follow least-privilege principles from day one. User IDs 1--999 are reserved for system and service accounts; regular users start at ID 1000.
Service Accounts¶
The system seeds a set of purpose-built service accounts during initial database setup (029_seed_data.sql). Each account has only the permissions required for its specific job, so backends never need to pass user_id 1 (the system superuser) at runtime.
| ID | Username | Display Name | Purpose |
|---|---|---|---|
| 1 | system |
System | Seed data and migrations. Has a hard-coded bypass in auth.has_permissions -- permission checks always return true for this user. Should not be used at runtime. |
| 2 | svc_registrator |
Registrator | User registration and email/phone verification token creation. |
| 3 | svc_authenticator |
Authenticator | Login flow, permission resolution, token validation. |
| 4 | svc_token_manager |
Token Manager | Full token lifecycle -- password reset, email verification, etc. |
| 5 | svc_api_gateway |
API Gateway | API key validation at gateway/middleware level. |
| 6 | svc_group_syncer |
Group Syncer | Background synchronization of external group members from identity providers. |
| 800 | svc_data_processor |
Data Processor | Generic application-level processing. Ships with an empty permission set -- add your own permissions as needed. |
All service accounts share these flags:
user_type_code = 'service'is_system = truecan_login = false
After the service accounts are created, the user_id sequence is reset to 1000 so that normal user registrations never collide with the reserved range:
The System User Bypass¶
The system user (ID 1) has special treatment inside auth.has_permissions:
This means every permission check automatically passes for user_id 1. The bypass exists so that seed scripts and migrations can execute any function without pre-assigning permissions. At runtime, applications should use the appropriate service account instead.
Never use user_id 1 at runtime
Passing 1 as the acting user skips all permission validation. Use the dedicated service account whose permission set matches the operation you need.
Using Service Accounts¶
Pass the service account's user_id 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);
Service Account Permission Sets¶
Each service account is assigned a dedicated permission set containing only the permissions it needs:
| Service Account | Permission Set | Permissions |
|---|---|---|
svc_registrator (2) |
svc_registrator_permissions |
users.register_user, users.add_to_default_groups, tokens.create_token |
svc_authenticator (3) |
svc_authenticator_permissions |
authentication.get_data, authentication.ensure_permissions, authentication.get_users_groups_and_permissions, authentication.create_auth_event, tokens.validate_token, tokens.set_as_used |
svc_token_manager (4) |
svc_token_permissions |
tokens.create_token, tokens.validate_token, tokens.set_as_used |
svc_api_gateway (5) |
svc_api_gateway_permissions |
api_keys.validate_api_key |
svc_group_syncer (6) |
svc_group_syncer_permissions |
groups.get_groups, groups.get_members, groups.create_member, groups.delete_member, groups.get_mapping, users.register_user, users.add_to_default_groups |
svc_data_processor (800) |
svc_data_processor_permissions |
(empty -- add application-specific permissions) |
Recommended default for application code
The svc_data_processor (ID 800) is the recommended starting point for application-specific backend operations. Add the permissions your application needs to the svc_data_processor_permissions perm set.
Technical Users (API Keys)¶
API keys provide service-to-service authentication. Each API key automatically creates a technical user in the auth.user_info table with user_type_code = 'api'.
How It Works¶
When auth.create_api_key() is called, the system:
- Generates or accepts an API key and secret.
- Inserts a row into
auth.api_key. - Calls
unsecure.create_api_user()which creates a newauth.user_inforow with usernameapi_key_<key>anduser_type_code = 'api'. - Assigns the requested permission set and/or individual permissions to that technical user.
-- create_api_user inserts a technical user
insert into auth.user_info (created_by, updated_by, user_type_code, code, username,
original_username, display_name)
values (_created_by, _created_by, 'api', __normalized_username,
__normalized_username, __normalized_username, __normalized_username);
Why Technical Users?¶
By giving every API key its own user_info record, the system can use the same auth.has_permission() and auth.has_permissions() functions for both human users and service integrations. There is no separate code path -- permission checks, permission caching, audit logging, and assignment all work uniformly.
This also means:
- API key permissions appear in the same
auth.permission_assignmenttable as user and group permissions. - Audit events (
auth.user_event,public.journal) record the technical user_id, making it easy to trace which API key performed an action. - Permission cache invalidation works identically for technical users.
Cross-reference
For the full API key function reference, see API Keys.
Human Admin Permission Sets¶
The system seeds composable permission sets for human administrators. Each set covers a specific administrative domain -- assign one or combine several for composite roles.
| Permission Set | Code | Domain | Key Permissions |
|---|---|---|---|
| User manager | user_manager |
User CRUD and audit | users, authentication.read_user_events, journal.read_journal, journal.get_payload |
| Group manager | group_manager |
Groups and membership | groups, journal.read_journal, journal.get_payload |
| Permission manager | permission_manager |
Permissions and perm sets | All permissions.* CRUD, journal.read_journal, journal.get_payload |
| Provider manager | provider_manager |
Identity providers | providers, journal.read_journal, journal.get_payload |
| Token manager | token_manager |
Token lifecycle and config | tokens.create_token, tokens.validate_token, tokens.set_as_used, token_configuration, journal.read_journal, journal.get_payload |
| Api key manager | api_key_manager |
API key CRUD | api_keys, journal.read_journal, journal.get_payload |
| Auditor | auditor |
Read-only audit access | journal, authentication.read_user_events, users.read_users, groups.get_group, groups.get_groups, tenants.read_tenants |
| Resource manager | resource_manager |
Resource-level ACL | resources, journal.read_journal, journal.get_payload |
| Full admin | full_admin |
Everything combined | All admin permissions including journal.purge_journal |
All sets (except Auditor) include journal.read_journal and journal.get_payload for audit visibility. The Auditor set has the full journal parent permission, which grants all journal sub-permissions including read access to global journal entries.
Additional Built-In Permission Sets¶
Beyond the composable admin sets, the system also seeds role-oriented sets for tenant operations:
| Permission Set | Code | Key Permissions |
|---|---|---|
| System admin | system_admin |
Superset of all domain permissions -- tenants, providers, users, groups, journal (including purge), API keys, languages, translations, tokens, authentication, resources |
| Tenant creator | tenant_creator |
tenants.create_tenant, journal.read_journal, journal.get_payload |
| Tenant admin | tenant_admin |
tenants, journal.read_journal, journal.get_payload, languages, translations |
| Tenant owner | tenant_owner |
groups, tenants.update_tenant, tenants.assign_owner, tenants.get_users, journal.read_journal |
| Tenant member | tenant_member |
tenants.get_groups, tenants.get_users |
Built-In Admin Groups¶
The seed data creates three system groups with permission sets pre-assigned:
| Group ID | Group Title | Permission Set |
|---|---|---|
| 1 | System admins | system_admin |
| 2 | Tenant admins | tenant_admin |
| 3 | Full admins | full_admin |
Bootstrapping Admin Access¶
The simplest way to give a user full administrative access is to add them to the "Full admins" group (ID 3):
-- add user 1001 to the Full admins group
select auth.create_user_group_member('admin', 1, null, 3, _target_user_id := 1001);
For more granular access, assign individual admin permission sets directly to a user:
-- assign user manager and group manager roles to user 1001
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');
Cross-reference
For details on permission set creation and assignment functions, see Permissions Reference.
User Types¶
The const.user_type table defines four user type codes. Every auth.user_info record has a user_type_code that determines how the account is used:
| Code | Purpose | Created By | can_login |
Example |
|---|---|---|---|---|
normal |
Regular human user | auth.register_user() or auth.ensure_user_from_provider() |
true |
End-users logging in via a UI |
system |
System superuser | unsecure.create_user_system() (seed data) |
false |
User ID 1 -- the hard-coded permission bypass account |
service |
Service account | unsecure.create_service_user_info() (seed data) |
false |
svc_registrator, svc_authenticator, etc. |
api |
Technical user for an API key | unsecure.create_api_user() (called by auth.create_api_key()) |
default | api_key_<key> -- one per API key |
Service vs API user types
Service accounts are pre-seeded during database setup and represent backend subsystems (registration, authentication, etc.). API users are created dynamically at runtime when a new API key is issued. Both use the same permission system, but service accounts have fixed IDs in the 1--999 range while API users get IDs from the normal sequence (1000+).
User Type Lifecycle¶
Database Setup (029_seed_data.sql)
|
+-- unsecure.create_user_system() --> user_type_code = 'system' (ID 1)
+-- unsecure.create_service_user_info() --> user_type_code = 'service' (IDs 2-6, 800)
|
+-- ALTER SEQUENCE ... RESTART WITH 1000
|
Runtime
|
+-- auth.register_user() --> user_type_code = 'normal' (ID >= 1000)
+-- auth.ensure_user_from_provider() --> user_type_code = 'normal' (ID >= 1000)
+-- auth.create_api_key() --> user_type_code = 'api' (ID >= 1000)