Skip to main content

Tokens

exd bearer credentials: format, types, lifecycle, and the HTTP API for managing them. Authorization decisions — who can call which endpoint — live in access-control.


Credential types

All API requests authenticate with Authorization: Bearer <credential>.

CredentialIssued byIntended use
Human session tokenLogin flow for an admitted tenant userBrowser UI and interactive CLI use. Effective permissions come from the user's tenant and flag-namespace roles.
Service tokenToken management API, admin UI, or exd-server-admin CLIAutomation, CI, SDK clients, agents, operational scripts. Permissions come from token type, binding, and optional scopes.

Namespace-scoped endpoints carry the owning tenant in the URL path (/api/v1/tenants/{tenant}/namespaces/{ns}/...); the server cross-checks the path's {tenant} against the authenticated session or service-token binding and rejects mismatches with 403 Forbidden. The token-management endpoints (/api/v1/tokens) live at the installation level because the token id is itself the resource and the tenant or namespace binding is recorded on the token record.


Service token types

TypePrefixBindingPermission summary
namespace-readexd_read_One (tenant, namespace)Download that flag namespace's manifest and call evaluation endpoints. Intended for the Rust SDK and read-only automation.
namespace-writeexd_write_One (tenant, namespace)All namespace-read permissions plus manifest upload, lint, rollback, and version metadata. Cannot create or delete flag namespaces.
namespace-clientexd_client_One (tenant, namespace, environment)Call /evaluate and /evaluate/all for the bound (tenant, namespace, environment) triple, and only when that environment has public_evaluate = true. Intended for browser bundles and other untrusted clients where the token cannot be kept secret.
tenant-adminexd_tenant_One tenantAdministrative automation across all flag namespaces in the bound tenant: namespace lifecycle, namespace token issuance, manifest operations, namespace-admin management.
superadminexd_admin_InstallationInstallation-wide administration across all tenants and flag namespaces. For bootstrap, break-glass, and tightly controlled platform automation.

Service tokens never become human users and are never members of tenant-admin or namespace-admin lists. They carry explicit token permissions and are bound to their issued scope. The canonical service-token permission matrix is access-control § Service token permissions.

Schema 1.0 defines no optional service-token scopes. The scopes field on token records is reserved for future use and MUST be the empty array.


namespace-client tokens

namespace-client tokens are designed for environments where the token secret cannot be kept secret — typically a JavaScript bundle shipped to a browser. Their security model differs from every other service-token type:

  • The secret is treated as public. Anyone who can fetch the JavaScript bundle can read the token. Authorization decisions MUST NOT rely on the token being unknown to attackers.
  • The token is bound to one (tenant, namespace, environment) triple, not just one flag namespace. The bound environment is recorded as environment_slug on the token record and is enforced on every evaluation request.
  • The bound environment MUST have public_evaluate = true in namespace.toml at the time of every authentication. If the manifest later flips it back to false, every namespace-client token bound to that environment fails immediately with 403 Forbidden — without revoking the token records — so manifest authors retain a kill switch independent of token rotation.
  • Permissions are limited to evaluate.public for the bound (tenant, namespace, environment) triple. The token cannot download the manifest, list versions, lint, or call any admin endpoint.
  • CORS is gated by an explicit Origin allowlist stored on the token record. The server only adds Access-Control-Allow-Origin headers for requests whose Origin matches an entry in allowed_origins. Native (non-browser) callers are unaffected by CORS but are still subject to all other restrictions.

Additional fields on namespace-client token records

FieldTypeDescription
environment_slugstringThe bound environment slug. Required at issuance for namespace-client; MUST be null for every other token type.
allowed_originsarray of stringsOrigin allowlist for browser-CORS responses. Each entry is an Origin value as defined by RFC 6454 (e.g., https://app.example.com). The wildcard "*" is rejected at issuance. May be empty for non-browser deployments; in that case the server emits no CORS headers.

namespace-client tokens MUST NOT carry any optional scope. Issuance rejects such requests with 400 invalid_request.

Issuance and rotation

Issued through POST /api/v1/tokens with type = "namespace-client", tenant_slug, namespace_slug, environment_slug, and allowed_origins in the request body. Authorization is identical to other namespace-bound tokens (namespace admin, tenant admin, tenant-admin token, or superadmin). The server additionally verifies that the named environment exists in the flag namespace's current manifest; environments not present in [namespace.environments] are rejected with 400 invalid_request.

Rotation, revocation, and lifecycle states behave identically to other service tokens. Because namespace-client tokens are public-by-design, operators SHOULD prefer short expires_at values (30–90 days) and use rotation rather than long-lived secrets, since revoking a leaked browser-embedded token requires a deploy in addition to the API call.

Telemetry and rate limiting

Successful authentications under a namespace-client token are tagged principal_type = "client" in audit and telemetry events so admins can distinguish browser traffic from server-side traffic. Rate limits for evaluation under namespace-client are scoped per (tenant, namespace, environment, Origin) rather than per token (see server-api § Rate limits) — one bad actor cannot meaningfully consume a flag namespace's quota by minting many Origin values they do not control on browsers they do not own.


Token format

Service tokens follow:

exd_<type>_<payload>

<payload> is 32 bytes of cryptographically random data encoded in Base58. The prefix is public metadata used for routing and operator recognition; it is not sufficient for authorization.

Example tokens:

exd_write_7gHkLmN3pQrStUvWxYz2AbCdEfGhJkLmNpQr3s
exd_read_4tRvBn9wKjMpXzQsUyAeCdFgHiJkLnOqRtWvYb
exd_client_8jKmPqRsTuVwXyZaBcDeFgHiJkLmNoPqRsTu
exd_tenant_5cNpQrStUvWxYzAbCdEfGhJkLmNoPqRsTuVwXz
exd_admin_2mNpQrStUvWxYzAbCdEfGhJkLmNoPqRsTuVwXy

The plaintext token is returned only once, immediately after creation or rotation. API responses after that expose only token metadata and a non-secret prefix.


Token records

The server stores token metadata separately from the token secret.

FieldTypeDescription
idstringStable opaque identifier, e.g. tok_01HV5Z9R4Y0XJ7A2W8WJ9R7S3A.
typestringOne of the service-token types.
namestringOperator-provided display name, unique within the bound scope.
descriptionstring | nullOptional operational note.
tenant_slugstring | nullBound tenant for tenant and flag-namespace tokens. null only for superadmin tokens.
namespace_slugstring | nullBound flag namespace for namespace tokens.
environment_slugstring | nullBound environment. Required for namespace-client; null for every other token type.
allowed_originsarray of stringsOrigin allowlist for namespace-client browser callers. Empty array [] for non-namespace-client tokens.
scopesarray of stringsReserved for future optional scopes. MUST be [] in schema 1.0.
prefixstringFirst 14 characters of the token, retained for audit/debug display.
created_bystringUser id or token id that created the token.
created_atstringRFC 3339 creation timestamp.
expires_atstring | nullExpiration timestamp. null means no automatic expiration.
last_used_atstring | nullLast successful authentication timestamp.
last_used_ip_hashstring | nullOptional hashed client IP for abuse investigation without storing raw IPs.
statusstringactive, revoked, or expired.
revoked_atstring | nullRevocation timestamp.
revoked_bystring | nullUser id or token id that revoked the token.

Full token values are never stored. The server stores a keyed HMAC-SHA-256 digest of the full token using a server-side secret, plus the public prefix. Authentication performs a constant-time digest comparison. Rotating the server-side HMAC key requires reissuing service tokens.


Issuance rules

Issuance always enforces least privilege:

Token to issueWho can issue it
namespace-readNamespace admin for that flag namespace, tenant admin for the owning tenant, tenant-admin token for the owning tenant, or superadmin.
namespace-writeSame as namespace-read.
namespace-clientSame as namespace-read. The bound environment MUST exist in the flag namespace's current manifest at issuance time; public_evaluate = true is NOT required at issuance (the check is per-request).
tenant-adminHuman tenant admin for that tenant or superadmin. A tenant-admin service token cannot mint another tenant-admin token.
superadminBootstrap/admin CLI on the server host, or an existing active superadmin token.

A token cannot issue a token with broader installation visibility than itself. A namespace-bound token cannot issue any token. A token cannot grant optional scopes it does not already have, except that a human admin can grant scopes permitted by their role.

Namespace creation does NOT automatically issue service tokens. Create the flag namespace first, then create explicit tokens with names, owners, scopes, and expiration appropriate for the consumer.


Lifecycle

States

StateMeaning
activeToken authenticates if its scope permits the requested operation.
expiredexpires_at is in the past. Returns 401 Unauthorized.
revokedManually revoked. Returns 401 Unauthorized.

Expired and revoked tokens are retained as metadata for audit history until the installation's audit-retention policy removes them.

Expiration

expires_at is optional. Operators should prefer bounded lifetimes for automation tokens:

Token typeRecommended max lifetime
CI namespace-write90 days
Runtime namespace-read SDK180 days, unless deployment rotation is difficult
namespace-client30–90 days. Embedded in publicly-served assets; rotation requires a deploy.
tenant-admin, superadminShortest practical lifetime; prefer human login where possible.

The server evaluates expiration on every request. Expiration does NOT delete the token record.

Revocation

Revocation is immediate. New requests using the token fail with 401 Unauthorized. In-flight requests that already passed authentication may complete normally.

Revoking a tenant or flag namespace deletes no tokens by itself, but tenant deletion or namespace deletion revokes all tokens bound to the deleted object.

Rotation

Rotation creates a replacement token with the same type, binding, scopes, and expiration policy unless overridden. The old token remains active until explicitly revoked. This supports zero-downtime credential rotation:

  1. Create or rotate to a new token.
  2. Update the consumer secret.
  3. Verify the consumer succeeds with the new token.
  4. Revoke the old token.

The server records rotated_from_token_id on the new token and rotated_to_token_id on the old token when rotation is performed through the rotation endpoint.

Audit events

The server records audit events for token lifecycle changes:

EventTrigger
token.createdA service token is created.
token.rotatedA replacement token is created through the rotation endpoint.
token.revokedA service token is revoked.
token.expiredA token is first observed or swept after expires_at.
token.authenticatedA token successfully authenticates. May be sampled or rate-limited; last_used_at is updated at most once per token per minute.

Audit entries include token id, token prefix, token type, tenant / flag-namespace binding, actor id, timestamp, request id, and result. They never include the full token secret.


Token management API

Token-management endpoints live at the installation level because the token itself carries the tenant or flag-namespace binding.

POST /api/v1/tokens

Create a service token.

Auth required: see Issuance rules and access-control § Token management authorization.

Request body:

{
"type": "namespace-write",
"name": "payments-ci-upload",
"description": "GitHub Actions manifest upload for payments",
"tenant_slug": "acme",
"namespace_slug": "payments",
"expires_at": "2026-07-24T00:00:00Z"
}
FieldRequiredDescription
typeyesService-token type.
nameyesHuman-readable name.
descriptionnoOptional note shown in admin UI and audits.
tenant_slugconditionalRequired for tenant-admin and every namespace token (namespace-read, namespace-write, namespace-client). Namespace slugs are unique per-tenant, not globally — tenant_slug is needed to disambiguate. Ignored for superadmin.
namespace_slugconditionalRequired for namespace tokens (including namespace-client). Must be omitted for tenant-admin and superadmin. The named flag namespace must exist in tenant_slug.
environment_slugconditionalRequired for namespace-client. Must be omitted for every other type. The named environment MUST exist in the flag namespace's current manifest.
allowed_originsconditionalOptional for namespace-client (defaults to []). Must be omitted or empty for every other type. Each entry is an Origin value; "*" is rejected.
scopesnoReserved for future optional scopes. MUST be omitted or [] in schema 1.0.
expires_atnoOptional RFC 3339 expiration timestamp.

Response: 201 Created

{
"token": {
"id": "tok_01HV5Z9R4Y0XJ7A2W8WJ9R7S3A",
"type": "namespace-write",
"name": "payments-ci-upload",
"tenant_slug": "acme",
"namespace_slug": "payments",
"scopes": [],
"prefix": "exd_write_7gHk",
"created_at": "2026-04-25T09:14:33Z",
"expires_at": "2026-07-24T00:00:00Z",
"last_used_at": null,
"status": "active"
},
"secret": "exd_write_7gHkLmN3pQrStUvWxYz2AbCdEfGhJkLmNpQr3s",
"request_id": "01HV5XK2GFQT8N3JRDCP7MW04B"
}

secret is shown only in this response.

GET /api/v1/tokens

List visible token metadata. Secrets are never returned.

Auth required: token.read for each returned token. Namespace-bound tokens cannot list tokens.

Query parameters:

ParameterDescription
tenantLimit to a tenant. Required when the caller can see multiple tenants and is not superadmin.
namespaceLimit to one flag namespace.
typeFilter by service-token type.
statusactive, revoked, or expired. Defaults to active.
limit, afterPagination (see server-api § Pagination).

GET /api/v1/tokens/{token_id}

Return metadata for one token visible to the caller. Secrets are never returned.

POST /api/v1/tokens/{token_id}/rotate

Create a replacement token.

Auth required: token.rotate on the token AND permission to create the replacement token type.

Request body:

{
"name": "payments-ci-upload-2026-q3",
"expires_at": "2026-10-22T00:00:00Z"
}

Omitted fields inherit from the old token. The response shape is the same as POST /api/v1/tokens and includes the new one-time secret.

DELETE /api/v1/tokens/{token_id}

Revoke a token immediately.

Auth required: token.revoke on the token, or the token itself for self-revocation. Namespace-bound tokens cannot revoke other tokens.

Response: 200 OK

{
"token": {
"id": "tok_01HV5Z9R4Y0XJ7A2W8WJ9R7S3A",
"status": "revoked",
"revoked_at": "2026-04-25T10:15:00Z"
},
"request_id": "01HV5XK2GFQT8N3JRDCP7MW04C"
}

Human session tokens

Human session tokens are issued by the configured tenant login flow:

  • SSO tenants use the configured OIDC or SAML provider.
  • Email-domain tenants use the installation's verified-email login flow.

Session tokens are short-lived bearer credentials. They encode or reference the authenticated user id, admitted tenants, session expiry, and login assurance. Effective authorization is computed on each request from current tenant-admin and namespace-admin membership (see access-control) — so revoking an admin role takes effect without waiting for the session to expire.

Human session refresh, cookie settings, MFA policy, and identity-provider configuration are installation concerns and are outside the service-token management API. They must still follow the same authorization outcomes documented in the server API.


See also