Service Users
Service Users
Section titled “Service Users”Purpose
Section titled “Purpose”Service users are UserAccount records that represent machine-to-machine API access rather than a person signing in
through the UI. They were introduced so an opaque API key can resolve to a first-class account in the Accounts domain.
That gives API-key traffic the same authorization model, audit identity, and SaaS-admin flag behavior as other
authenticated actors, without pretending that the caller is an interactive human user.
The core idea is deliberately narrow:
- a service user is a
UserAccountwithisService = true; - the API key is never stored directly on the user account;
- the configured SHA of the API key is stored in
sha; - the service user’s email address is the stable configured identity for that API key;
- service users are managed from application configuration, not from the UI.
The CHANGELOG.md entry summarizes this as: API keys are mapped to dedicated service users; those users cannot log in
through the UI and can only access the API. This document expands that concept for maintainers and operators.
Data Model
Section titled “Data Model”The service-user change adds two fields to UserAccount:
sha: Sha?isService: Boolean
sha is the lookup key derived from the presented opaque token. isService marks the record as a service account
rather than a person. These fields sit alongside existing account properties such as oidcSub, isSaasAdmin, identity,
settings, subscription, and active agency.
The identity of a service user is intentionally stable. The email address, eId, and oidcSub of a UserAccount are
immutable. This matters because these values are used as durable references in audit trails, actor resolution, and other
domain records. If an API key needs to represent a different identity, the safe model is to configure a new service user
or let the old one be pruned, not to mutate the core identity of the existing user.
Service users also receive normal account fields:
subscriptionis active when the service user is created by the system;isSaasAdminis controlled by configuration;identity.emailis controlled by configuration;oidcSubis generated by the system when the service user is created.
No personal tenant or AgentFor record is created as part of automatic service-user creation. Service users exist to
authenticate API access and resolve actor identity, not to onboard a person into a tenant workflow.
Configuration
Section titled “Configuration”Service users are configured under the user-account module extras:
userAccount { extras { services { "sha256_<hash>" { emailAddress = "service-name@example.com" isSaasAdmin = false } } }}The map key is the SHA used to look up the service user. The value supplies the account identity and whether that service account should be treated as a SaaS admin.
Only configured service users should remain active. If configuration changes remove a service email, the pruning migration deletes the stale service-user account. This makes configuration the source of truth for the allowed set of machine users.
Authentication Flow
Section titled “Authentication Flow”When a request is authenticated with an opaque token, the endpoint actor interceptor treats it as service
authentication.
The token is hashed, prefixed with sha256_, and used to build a ServiceActor. The actor is then enriched through
UserAccountService.findUserBySha.
That lookup has four cases:
-
The SHA is not configured. The request cannot be resolved to a service user and fails authentication.
-
The SHA and configured email match an existing user. The existing user is returned after synchronizing mutable service-user attributes such as
sha,isService, andisSaasAdmin. -
The configured email exists but the SHA is missing. The existing account is converted into a service user by setting service-specific mutable fields. Its immutable identity remains unchanged.
-
No configured email exists. A new service user is created with the configured email, generated
oidcSub, configuredsha, configuredisSaasAdmin, and active subscription.
The lookup intentionally includes the configured email when matching by SHA. This avoids accidentally reusing an old service user if a SHA is reassigned to a different configured email. The email is the configured identity boundary; the SHA is the credential lookup key.
Actor Semantics
Section titled “Actor Semantics”Before this change, opaque-token authentication could only produce a generic service actor. With service users, it resolves to an actor that carries account-level details:
- service SHA;
oidcSub;userEid;userEmail;isSaasAdmin.
This means downstream authorization can reason about service callers using the same account-derived fields available for
human users. The actor identity used for logs and authorization messages is still explicit: service actors identify as
service <sha>, while user actors identify as user <subject>.
Service actors are not tenant actors. They do not receive tenant membership through AgentFor enrichment. Where service
access is allowed, domain logic should handle ServiceActor deliberately. Where tenant membership is required, a
service
actor should not silently pass as a tenant user.
Lifecycle and Pruning
Section titled “Lifecycle and Pruning”Service users are created lazily. The system does not pre-seed every configured service user at startup. Instead, the first authenticated request for a configured SHA resolves or creates the corresponding account.
Startup does perform cleanup. The ServiceUserPruningMigrationService runs as a startup migration and calls
UserAccountService.pruneTechnicalUser(). Despite the legacy method name, the behavior is service-user pruning:
- list all
UserAccountrecords withisService = true; - build the configured set of service-user email addresses from
servicesBySha; - delete any service user whose email is no longer configured.
The migration records deletions in MigrationStats.deleted and logs a ServiceUserPruning migration summary. This
makes
startup cleanup consistent with the existing startup migration pattern used by AgentFor subject backfill.
Pruning by email rather than SHA is intentional. A key can rotate, which changes the SHA, but the service identity can
remain the same. If the configured email is still present, the account remains valid and findUserBySha can synchronize
the new SHA on the next lookup. If the email disappears from configuration, the service identity is no longer allowed
and
the old service user should be deleted.
Operational Guidance
Section titled “Operational Guidance”To add a service user:
- choose a stable email-like identity for the service;
- configure the SHA of the opaque token under
userAccount.extras.services; - set
isSaasAdminonly when the service must operate with SaaS-admin privileges; - deploy the configuration.
To rotate an API key:
- update the configured SHA for the same email;
- deploy the configuration;
- allow the next request to synchronize the service user’s
sha.
To remove a service user:
- remove its email entry from configuration;
- deploy the configuration;
- let startup pruning delete the stale service-user record.
Avoid changing a configured service user’s email to mean “same service, new address.” Email is treated as immutable account identity. If the identity really changes, configure the new email and let the old one be pruned.
Implementation Notes
Section titled “Implementation Notes”- Helm’s rich function library makes it simple and safe to configure service users. Use Helm
sha256sumfunction to define the service account in deployment secrets and keep it always synchronized with the actual API key.
Example:
userAccount.extras.services.sha256_{{ sha256sum "{{ .ArdaApiKey }}" }}.emailAddress=service+arda@arda.cardsuserAccount.extras.services.sha256_{{ sha256sum "{{ .ArdaApiKey }}" }}.isSaasAdmin=true- This implementation prefixes the SHA with
sha256_to enable other SHA algorithms in the future.
Security Notes
Section titled “Security Notes”Service users are powerful because they translate API keys into first-class authenticated actors. Their configuration should therefore be handled like other security-sensitive deployment configuration:
- store API keys in secret management, not in source;
- configure only the SHA in Accounts;
- grant
isSaasAdminsparingly; - use dedicated service-user emails that make ownership obvious;
- remove unused service users from configuration rather than leaving dormant identities around.
Service users reduce ambiguity. A request authenticated by API key is no longer merely “some service”; it resolves to a specific configured account, with a durable account ID and subject for audit and authorization behavior.
Copyright: © Arda Systems 2025-2026, All rights reserved