Skip to content

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 = true
  • can_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:

alter sequence auth.user_info_user_id_seq restart with 1000;

The System User Bypass

The system user (ID 1) has special treatment inside auth.has_permissions:

if (_target_user_id = 1)
then
    return true;
end if;

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:

  1. Generates or accepts an API key and secret.
  2. Inserts a row into auth.api_key.
  3. Calls unsecure.create_api_user() which creates a new auth.user_info row with username api_key_<key> and user_type_code = 'api'.
  4. 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_assignment table 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)