Permission model and API key format
On this page
- Provenance and clean-room citations
- 1. Specification summary
- 2. Permission catalog
- Core permissions
- Catalog endpoint
- 3. Role hierarchy and structure
- Schema definitions
- Standard roles
- Role assignment and execution context
- 4. API key format and cryptographic properties
- Format specification
- Storage and lookup mechanics
- 5. Scope rules and application isolation
- Application scoping (appNames / appIds)
- Permission scoping
- 6. No-auth mode
- Invariants in no-auth mode
- Audit log
This document specifies the authorization model, permission catalog, role
hierarchy, API key structure, and no-auth mode semantics for Relay. The
specifications are derived under clean-room rules from the permitted DBOS
Transact SDKs, the dbosctl command-line client, and the Conductor OpenAPI
specification.
Provenance and clean-room citations
All specifications in this document are derived from permitted sources:
-
dbosctl client source and OpenAPI specification: Repository
https://github.com/dbos-inc/dbos-ctl, commit1e6d20e3201588ff67d6157f3808c933b3ed5811- API key management:
internal/cli/apikey.go(lines 13-19, 48-50, 78-118, 141-156) - Permission inspection:
internal/cli/permission.go(lines 9-20, 27-48) - Identity resolution:
internal/cli/whoami.go(lines 37-41, 61-94) - Client transport:
internal/client/client.go(lines 28-32, 54-60) - Configuration resolution:
internal/config/resolve.go(lines 103-109) - OAuth-gated operation registry:
internal/api/oauth_gated.go(lines 9-26) - OpenAPI schemas and routes:
api/spec/openapi.json- Schemas:
Token(#/components/schemas/Token),TokenCreated(#/components/schemas/TokenCreated),CreateTokenInputBody(#/components/schemas/CreateTokenInputBody),RoleOutput(#/components/schemas/RoleOutput),CreateRoleInputBody(#/components/schemas/CreateRoleInputBody),CreateRoleOutputBody(#/components/schemas/CreateRoleOutputBody) - Paths:
#/paths/~1v2~1orgs~1{orgName}~1permissions,#/paths/~1v2~1orgs~1{orgName}~1tokens,#/paths/~1v2~1orgs~1{orgName}~1roles
- Schemas:
- Conformance tests:
internal/cli/apikey_test.go(lines 16-35, 78-134),internal/cli/permission_test.go(lines 45-61, 79-97),internal/cli/integration_test.go(lines 133-174, 403-429)
- API key management:
-
dbos-transact-golang: Repository
https://github.com/dbos-inc/dbos-transact-golang, commitfb3e33e0b4c3c709b9271eb935adce5eaf9386f5- WebSocket connection URL and key transmission:
dbos/conductor.go(lines 40, 78-95) - Configuration defaults:
dbos/dbos.go(lines 711, 727) - Role tracking in workflow contexts:
dbos/workflow.go(lines 37, 1507, 1529, 2117) - System database persistence:
dbos/internal/sysdb/system_database.go(lines 1487, 1792-1848)
- WebSocket connection URL and key transmission:
-
dbos-transact-py: Repository
https://github.com/dbos-inc/dbos-transact-py, commit2b93e1467a5464f817ef5d11aa5f10d3d2253761- WebSocket connection URL:
dbos/_conductor/conductor.py(line 63) - Configuration options:
dbos/_dbos.py(DBOSconfiguration),dbos/_dbos_config.py(DBOSConfigline 17,RuntimeConfigline 92)
- WebSocket connection URL:
-
dbos-transact-ts: Repository
https://github.com/dbos-inc/dbos-transact-ts, commit749a4d420127e97715bf1f5d8caabf496a40af0b- Protocol serialization:
src/conductor/protocol.ts(WorkflowsOutputline 195,WorkflowStepsline 264) - Context and execution roles:
src/context.ts(DBOSContextOptionsline 25),src/system_database.ts(authenticatedUserline 307,authenticatedRolesline 346)
- Protocol serialization:
-
dbos-transact-java: Repository
https://github.com/dbos-inc/dbos-transact-java, commitecc2bda4deb57e3ba38c55cca150e95c99eb9d64- Default role strings:
transact/src/test/java/dev/dbos/transact/database/SystemDatabaseTest.java(lines 818-819, 838-839) - Queue role validation:
transact/src/test/java/dev/dbos/transact/queue/DynamicQueuesTest.java(lines 1028-1029)
- Default role strings:
1. Specification summary
Summary of permissions, default roles, application scoping, and key format confirmed from public documentation and client source:
- Permission list:
application.read,application.write,websocket.connect,metric.read,organization.read,organization.write,token.read, andtoken.writeform the grantable permission catalog exposed byGET /v2/orgs/{orgName}/permissions. - Default roles:
admin,operator, andviewer(read-only), withadminalso represented as a user profile attribute (isDbosAdmin). - Key scoping: Keys are scoped by organization, optionally restricted to
one or more application names via
appNames(appIdsin responses), and optionally constrained to a specific permission subset. - Key prefix: Plaintext API keys begin with the prefix
dbos_.
2. Permission catalog
Permissions are atomic strings granting capability over resources within an organization.
Core permissions
| Permission | Scope | Operations permitted |
|---|---|---|
application.read |
Application | Read-only inspection of applications, workflows, execution steps, workflow events, notifications, streams, queues, schedules, metrics, alerting rules, and autoscaling policies. Backs dbosctl app list, dbosctl workflow list, dbosctl queue list, and dashboard monitoring. |
application.write |
Application | Mutations on application resources: registering apps, deleting apps, updating settings, cancelling workflows, resuming workflows, forking workflows, importing workflows, bulk operations (bulk-cancel, bulk-delete, bulk-resume, bulk-fork), schedule management (pause, resume, trigger, backfill), alerting rule creation and deletion, autoscaling policy modification. |
websocket.connect |
Application | Establishing executor WebSocket connections to the control plane at /websocket/{appName}/{apiKey}. Required by runtime worker processes. |
metric.read |
Application | Reading application metrics, including the Prometheus-compatible /v1/metrics scrape endpoint. Accepted alongside application.read on that endpoint. |
organization.read |
Organization | Read-only inspection of organization details, members, roles, domain claims, and audit logs. |
organization.write |
Organization | Mutations on organization resources: renaming the organization, managing members and roles, and setting audit retention. |
token.read |
Organization | Listing API keys. |
token.write |
Organization | Creating and revoking API keys. |
Catalog endpoint
The permission catalog is fetched via:
GET /v2/orgs/{orgName}/permissions(listPermissions)- Tag:
Roles - Auth requirement: Not OAuth-gated (
x-dbos-requires-oauth: false). Served in both authenticated and no-auth modes. - Response: JSON array of strings (
[]string), containing the supported grantable permissions:["application.read", "application.write", "websocket.connect", "metric.read", "organization.read", "organization.write", "token.read", "token.write"].
3. Role hierarchy and structure
Roles aggregate permissions into assignable identities.
Schema definitions
Defined in openapi-3.1.json:
RoleOutput(#/components/schemas/RoleOutput):name(string, required): Role identifier. Length 3 to 30 characters, matching pattern^[a-zA-Z0-9_]+$.permissions(array of string, required): Permissions granted to the role.isGlobal(boolean, required): Distinguishes built-in global roles from custom organization-level roles.
CreateRoleInputBody(#/components/schemas/CreateRoleInputBody):name(string, required): Length 3 to 30 characters, pattern^[a-zA-Z0-9_]+$.permissions(array of string, nullable): Initial permission set.
Standard roles
| Role name | isGlobal |
Permissions granted | Purpose |
|---|---|---|---|
admin |
true |
All eight permissions | Full administrative access. Can manage members, roles, domain claims, and API keys. Users with global administrative rights carry isDbosAdmin: true in UserProfile. |
operator |
true |
All eight permissions | Operational lifecycle management. Can run migrations, register apps, cancel/resume/fork workflows, manage schedules and queues, and manage organization resources. |
viewer |
true |
application.read, metric.read, organization.read, token.read |
Read-only inspection across applications, workflows, queues, schedules, metrics, organization resources, and API keys. Cannot alter operational state or connect executors. |
Relay's three global roles are a local design: upstream documents Organization Admin (all permissions) and Organization Member (all but organization.write) on the permissions page (https://docs.dbos.dev/production/permissions, confirmed 2026-09-21). Relay's operator matches the admin permission set for operational continuity, and the viewer holds the four read permissions.
Custom organization roles are never auto-upgraded: migration 0007 updates only the global rows, so a custom role cloned from an older admin, operator, or viewer keeps its original permission set until an administrator grants the new permissions explicitly.
Role assignment and execution context
- Organization membership roles are assigned via
PUT /v2/orgs/{orgName}/members/{username}/roles/{roleName}(grantRole). - During workflow execution, active user roles are captured in the execution
context (
authenticatedRoles: []string) and persisted in the application system database (workflow_status.authenticated_rolescolumn) as JSON arrays (for example,["admin", "operator"]).
4. API key format and cryptographic properties
API keys (referred to as tokens in Conductor REST paths) authenticate automated clients and SDK executors.
Format specification
| Parameter | Specification | Rationale |
|---|---|---|
| Key prefix | dbos_ |
5 ASCII characters. Matches upstream convention verified across SDKs, CLI, and integration tests (apikey_test.go:80, integration_test.go:148). Allows secret scanners to detect exposed keys. |
| Entropy | 32 bytes (256 bits) | Generated via a cryptographically secure pseudorandom number generator (crypto/rand). Provides 256 bits of security against brute-force search. |
| Alphabet | Base64 URL (unpadded) | RFC 4648 URL-safe alphabet ([A-Za-z0-9_-]). Eliminates URL-encoding issues when transmitted in HTTP headers or WebSocket URL path segments. |
| Encoded length | 43 characters | Base64 encoding of 32 bytes unpadded: ceil(32 * 8 / 6) = 43 characters. |
| Total plaintext length | 48 characters | 5 characters (dbos_) + 43 characters base64url = 48 characters total. |
| Lookup prefix | 12 characters | First 12 characters of the plaintext key (dbos_ + 7 characters base64url). Stored cleartext in database index. |
| Storage hash | SHA-256 (32 bytes) | SHA-256 digest of full plaintext key. Stored as binary BYTEA (or 64-character lowercase hex string). |
Storage and lookup mechanics
- Cleartext lookup prefix:
- Stored in the
api_keys.lookupcolumn with aUNIQUEindex. - Enables
O(1)database row lookup on incoming authentication requests. - Does not contain sufficient entropy to authenticate the caller.
- Stored in the
- Secret hash storage:
- Plaintext key is hashed with SHA-256:
hash = sha256(plaintext). - Stored in
api_keys.key_hash. - Verification uses constant-time comparison (
crypto/subtle.ConstantTimeCompare) between the candidate key's SHA-256 digest and the stored digest.
- Plaintext key is hashed with SHA-256:
- Single presentation:
- The plaintext key is returned exactly once in the response to
POST /v2/orgs/{orgName}/tokens/{tokenName}(TokenCreated.token). - The CLI outputs the raw key on stdout and prints a warning on stderr:
API key "..." created: store this secret now, it is not shown again(apikey.go:123). - The plaintext is never persisted in Relay's database and never logged.
- The plaintext key is returned exactly once in the response to
- Password hash vs cryptographic hash:
- High-entropy keys (256 bits of CSPRNG entropy) have no dictionary or low-entropy attack vectors.
- Slow key-derivation functions (such as bcrypt, scrypt, or argon2) are unnecessary and would severely bottleneck WebSocket connection handshakes and high-throughput API endpoints. SHA-256 provides immediate validation with constant-time comparison.
5. Scope rules and application isolation
API keys are scoped within an organization along two dimensions: application names and permissions.
Application scoping (appNames / appIds)
In CreateTokenInputBody:
{
"appNames": ["order-service", "billing-service"],
"permissions": ["application.read", "websocket.connect"]
}
- Unscoped keys (
(all)):- When
appNamesis omitted or empty (nil), the key is unscoped. - An unscoped key is valid for all applications within the owning organization.
- In CLI output: displayed as
(all)(apikey.go:179-185).
- When
- Scoped keys:
- When
appNamescontains one or more application identifiers, the key is strictly restricted to the specified applications. - Requests targeting an unlisted application receive
403 Forbidden. - WebSocket connection attempts at
/websocket/{appName}/{apiKey}whereappNamedoes not match the key's allowed applications are rejected during the upgrade handshake.
- When
Permission scoping
- When
permissionsis specified in key creation, the key possesses only the explicitly granted subset. - When
permissionsis omitted or empty, the key inherits the default capabilities of the creator's role within the organization.
6. No-auth mode
Relay supports running with authentication disabled, corresponding to self-hosted local deployments where no OIDC identity provider is configured.
Invariants in no-auth mode
- Default organization:
- The organization defaults to
"local"(internal/config/resolve.go:107-109). - All registered applications and workflows belong to
"local".
- The organization defaults to
- Implicit full permissions:
- Every incoming HTTP request to registered operational endpoints carries
implicit full permissions (
application.read,application.write,websocket.connect) for the"local"organization. - No
Authorization: Bearer <token>header is required.
- Every incoming HTTP request to registered operational endpoints carries
implicit full permissions (
- Executor WebSocket connection:
- WebSocket URL path:
/websocket/{appName}/{apiKey}. - In no-auth mode, the
{apiKey}path parameter is accepted unconditionally. Executors can supply any string (for example,"local","none", or"test-key").
- WebSocket URL path:
- Absence of OAuth-gated endpoints:
- Operations marked with
x-dbos-requires-oauth: truein the OpenAPI specification respond with 404 Problem Details from their handlers in no-auth mode, and any HTTP method targeting these routes is rejected with 404. - Probing or invoking an OAuth-gated endpoint returns
404 Not Found. - The server never returns
401 Unauthorizedor403 Forbiddenfor unregistered identity endpoints. - Gated operations (16 total, per upstream dbos-ctl
internal/api/oauth_gated.go):createRole(POST /v2/orgs/{orgName}/roles)deleteRole(DELETE /v2/orgs/{orgName}/roles/{roleName})generateSecret(POST /v2/orgs/{orgName}/secrets)getCurrentUser(GET /v2/users/me)getOrg(GET /v2/orgs/{orgName})grantRole(PUT /v2/orgs/{orgName}/members/{username}/roles/{roleName})joinOrg(POST /v2/orgs/{orgName}/join)listAuditLogs(GET /v2/orgs/{orgName}/audit-logs)listDomainClaims(GET /v2/orgs/{orgName}/domain-claims)listMembers(GET /v2/orgs/{orgName}/members)listRoles(GET /v2/orgs/{orgName}/roles)registerUser(POST /v2/users)releaseDomainClaim(DELETE /v2/orgs/{orgName}/domain-claims/{domain})removeMember(DELETE /v2/orgs/{orgName}/members/{username})requestDomainClaim(POST /v2/orgs/{orgName}/domain-claims)updateOrg(PATCH /v2/orgs/{orgName})
- Operations marked with
- Permissions route available:
GET /v2/orgs/{orgName}/permissionsis not OAuth-gated.- In no-auth mode, it responds with status
200 OKand the standard permission catalog (["application.read", "application.write", "websocket.connect", "metric.read", "organization.read", "organization.write", "token.read", "token.write"]).
- Local identity resolution:
dbosctl whoamiinspects the profile's auth setting (AuthNone).- Because
/v2/users/meis not registered,dbosctl whoamidirectly renders the static identity without making a network request:{"name": "local", "orgName": "local"}(whoami.go:37-41, 61-94).
Audit log
Relay records an audit entry for every mutating operation in the Conductor
taxonomy (https://docs.dbos.dev/production/audit-logs): application
registration, update, deletion, and latest-version changes; workflow cancel,
resume, fork, rewind, fork-from-failure, delete, import, and bulk operations;
schedule pause, resume, trigger, and backfill; alerting rule creation and
deletion; API key creation, rename, and revocation; role creation, deletion, and
grants; organization updates; user joins and removals; and autoscaling
policy setting and deletion (recorded as autoscaling_policy.set and
autoscaling_policy.delete, Relay-chosen names because the upstream
taxonomy publishes no strings for them). Reads are never
audited. Each entry records success or failure, so denied and invalid
requests appear alongside completed mutations, except where the
organization itself cannot be resolved (there is no scope to record
under) and except join-secret generation, which has no upstream operation
and is not recorded.
Entries store a details envelope with the subject (subject_type of user
or api_key, stable subject_id, and human-readable subject_display
preserved after deletion), the target (target_type and target_id,
omitted for bulk operations), the caller IP, and operation-specific
context (application_name, workflow_ids, permissions,
applications, role_name, new_name, audit_log_retention_days,
private_mode). Listing (GET /v2/orgs/{orgName}/audit-logs) accepts
startTime, endTime, operation (exact match), subject (matched
against display name, stable id, and legacy username column), and target
(exact target id) filters with
limit (default 100, maximum 1000, larger values rejected) and offset
paging, newest first.
Retention defaults to 90 days and is configurable per organization between
7 and 3650 days via PATCH /v2/orgs/{orgName} (audit_log_retention_days;
new_name renames the organization). A PATCH changing nothing returns 204
without recording an entry. Expired entries are purged hourly.
Domain-claim mutations are recorded under domain_claim.create and
domain_claim.delete, and join-secret generation under
secret.generate, Relay extensions outside the upstream taxonomy.
User registration (POST /v2/users) records no entry: audit entries are
scoped to an organization and no org context exists at registration time.
The subsequent join and role-grant actions are recorded.