Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.emergence.ai/llms.txt

Use this file to discover all available pages before exploring further.

Service Accounts

Service accounts allow non-interactive processes, background workers, schedulers, cleanup jobs, and automated pipelines, to authenticate with CRAFT using machine identity. Service accounts authenticate against the master realm in Keycloak (not the organization realm), which gives them platform-level access scoped by explicit organization and project headers.

Detection

The platform identifies a request as coming from a service account when:
  1. The client ID in the JWT begins with the svc- prefix (e.g., svc-data-pipeline)
  2. The JWT contains the serviceAccount realm role
  3. The authentication was performed against the Keycloak master realm
Non-interactive processes using regular user tokens are rejected at service account-protected endpoints.

Authentication Flow

1

Obtain a Client Credentials Token

Service accounts use the OAuth 2.0 Client Credentials grant, no user interaction required.
curl -X POST \
  "https://api.example.com/keycloak/realms/master/protocol/openid-connect/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=svc-my-worker" \
  -d "client_secret=<client-secret>"
Response:
{
  "access_token": "<jwt-token>",
  "token_type": "Bearer",
  "expires_in": 3600
}
2

Pass Service Account Context Headers

Service account JWTs do not carry tenant context. Pass these required headers:
Authorization: Bearer <jwt-token>
X-Org-Id: org_abc123
X-Project-ID: proj_xyz789
  • X-Org-Id, the organization context (required; replaces the realm-derived org in user tokens)
  • X-Project-ID, the project scope for project-scoped endpoints (required when applicable)
To attribute the request to a specific user in audit logs, also include X-On-Behalf-Of: <user-id> (optional).
3

Acting on Behalf of a User (optional)

The X-On-Behalf-Of header is recorded in audit logs but does not grant additional permissions. Include it whenever the service account is operating on a user’s behalf. The get_headers() helper accepts it as an optional parameter.

Creating a Service Account

1

Create a Confidential Client in Keycloak

In the Keycloak master realm admin console:
  1. Navigate to Clients → Create client
  2. Set Client ID with the svc- prefix (e.g., svc-nightly-cleanup)
  3. Set Client Protocol: openid-connect
  4. Set Access Type: confidential
  5. Enable Service Accounts Enabled
  6. Save and note the generated client secret
2

Assign the serviceAccount Role

In the client’s Service Account Roles tab:
  1. Select Realm Roles
  2. Assign the serviceAccount role
This role is required for the platform to recognize the client as a service account.
3

Grant Platform Permissions

Service accounts access the platform APIs with the same permission model as users. Grant the service account access to specific organizations and projects via the platform admin API or the Runtime UI.

Use Cases

Use CaseExample
Scheduled data pipelinesNightly data ingestion jobs that create agents and data connections
Background cleanupPurging expired sessions, rotating secrets, archiving old artifacts
Workflow orchestrationPrefect workflows that call platform APIs to register results
Automated testingCI/CD pipelines that create isolated test resources per run
Inter-service communicationSolution services calling platform APIs

Token Expiry and Rotation

Service account tokens are short-lived (configurable, default 1 hour). Background workers should implement automatic token refresh:
import httpx
import time

class ServiceAccountClient:
    def __init__(self, client_id: str, client_secret: str, keycloak_url: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.keycloak_url = keycloak_url
        self._token = None
        self._expires_at = 0

    def _refresh_token(self):
        response = httpx.post(
            f"{self.keycloak_url}/realms/master/protocol/openid-connect/token",
            data={
                "grant_type": "client_credentials",
                "client_id": self.client_id,
                "client_secret": self.client_secret,
            }
        )
        data = response.json()
        self._token = data["access_token"]
        self._expires_at = time.time() + data["expires_in"] - 60  # 60s buffer

    @property
    def token(self) -> str:
        if not self._token or time.time() >= self._expires_at:
            self._refresh_token()
        return self._token

    def get_headers(self, org_id: str, project_id: str, on_behalf_of: str | None = None) -> dict:
        headers = {
            "Authorization": f"Bearer {self.token}",
            "X-Org-Id": org_id,
            "X-Project-ID": project_id,
        }
        if on_behalf_of:
            headers["X-On-Behalf-Of"] = on_behalf_of
        return headers

Authentication

Overview of all authentication methods including user tokens and OIDC.

Authorization

How permissions are checked for service account requests.

Projects

Project-level isolation and the X-Project-ID header.