Sprinter Docs

API Keys

API key generation, validation, scope enforcement, and RBAC permission resolution for external system integration.

API Keys

API Keys enable external systems, AI agents, and scripts to authenticate with the platform without a user session. Each key is scoped to a tenant, tied to its creating user for permission resolution, and restricted to specific API scopes. Effective permissions are always bounded by what the creating user is allowed to do — a key can never escalate beyond the creator's role.

Overview

The module lives in features/api-keys/ and provides key generation (SHA-256 hashed, only shown once), validation middleware, scope enforcement, permission resolution, and an admin UI for managing keys. API keys are stored in the api_keys table with hashed secrets — the plaintext key is only returned at creation time.

External consumers include Claude Code, OpenClaw agents, MCP tool servers, CI/CD pipelines, and any HTTP client that needs programmatic access to the platform's API routes.

Key Concepts

API Key Format -- Keys use the prefix sk_ followed by 32 random bytes encoded as base64url. The first 12 characters are stored as key_prefix for identification in the UI. The full key is hashed with SHA-256 and stored as key_hash.

ApiScope -- Granular permission scopes that restrict what an API key can do:

ScopeDescription
entities:readList and fetch tenant records
entities:writeCreate, update, or delete records
entity-types:readList and fetch entity type schemas
entity-types:writeCreate or update entity type schemas
extraction:triggerStart extraction workflows
extraction:submitSubmit extracted field values
extraction:reviewApprove or reject extraction results
documents:readList and fetch documents
documents:writeUpload, update, or delete documents
relations:readFetch entity relations
relations:writeCreate or delete entity relations
skills:readList and fetch skills
views:readList and fetch views
tools:executeRun tool endpoints
chat:createStart chat sessions
*Full access — all of the creating user's permissions

Default scope for new keys: extraction:submit.

RBAC via creating user -- Effective permissions are computed as the intersection of:

  1. The permissions granted by the creating user's role in the tenant
  2. The permissions implied by the key's assigned scopes

This means an owner-created key with entities:write scope gets owner-level entity write permissions. A guest-created key with entities:write scope gets only the entity write permissions that guest members hold (potentially none). The key never exceeds the creator's ceiling.

The * scope bypasses scope filtering entirely — the key carries all permissions the creating user holds. This is the appropriate scope for trusted internal automation.

ApiKeyAuthContext -- The context object returned when a key is validated:

interface ApiKeyAuthContext {
  tenantId: string;
  scopes: ApiScope[];
  /** UUID of the user who created this key (permission ceiling + activity attribution). */
  createdBy: string | null;
  /** API key record ID (for audit logging). */
  keyId: string;
}

How It Works

Key Generation

The createApiKey() server action:

  1. Generates 32 random bytes, encodes as sk_ + base64url
  2. Extracts the first 12 chars as key_prefix
  3. Hashes the full key with SHA-256
  4. Stores the hash, prefix, scopes, and created_by (the current user's ID) in api_keys
  5. Returns ApiKeyWithSecret (view + plaintext secret, shown only once)

Request Authentication

The authenticateApiKey() middleware function in features/api-keys/lib/middleware.ts extracts an API key from either:

  • Authorization: Bearer sk_... header
  • X-API-Key: sk_... header

It calls validateApiKey(), which:

  1. Hashes the provided key
  2. Looks up the hash in api_keys (using admin client for cross-tenant access)
  3. Checks the key is not revoked and not expired
  4. Updates last_used_at (fire-and-forget)
  5. Returns ApiKeyAuthContext (including createdBy) or null

Permission Resolution

After authentication, API routes call resolveApiKeyPermissions() from features/api-keys/lib/permissions.ts to get the effective permission set:

const permissions = await resolveApiKeyPermissions(apiKeyCtx, membership);
// permissions = intersection of user role permissions AND scope-derived permissions

The function:

  1. Looks up the creating user's roleId via lookupUserMembership(). Falls back to ROLE_IDS.member if createdBy is null (keys created before the schema change).
  2. Fetches the role's permission set via getPermissionsForRole(roleId).
  3. Expands the key's scopes to a permission set via expandScopePermissions(scopes).
  4. Returns the intersection (both conditions must be satisfied).
  5. If scopes include *, skips step 3–4 and returns the full role permission set.

The resolved permissions are passed to resolveAgentTools(config, permissions) so MCP tools and entity tools respect the same RBAC gate as human sessions.

Scope-to-Permission Mapping

SCOPE_TO_PERMISSIONS in features/api-keys/lib/scopes.ts defines what each scope grants:

ScopePermissions granted
entities:readentities.own.read, entities.team.read, entities.all.read
entities:writeAll entity create, update, delete at own, team, all levels
extraction:submitentities.team.read, entities.team.update, responses.team.create
tools:executeFull read/write entity permissions, entity types read, responses create, documents read, agents read, views read
*Bypasses the map — returns all creating-user permissions unchanged

Activity Attribution

buildTenantContextForApiKey() constructs the tenant context for API key requests. Activity records (entity creates, updates, deletes) are attributed to the createdBy user with their actual tenant role. If the key has no createdBy, a synthetic anonymous identity is used with member role.

Scope Enforcement

API route handlers check scopes using hasScope(grantedScopes, requiredScope) from features/api-keys/lib/scopes.ts. The wildcard scope * grants access to every named scope. The middleware module provides unauthorizedResponse() and forbiddenScopeResponse() helpers for consistent error formatting.

Key Revocation

revokeApiKey(id) sets revoked_at on the key record. Revoked keys fail validation immediately. There is no un-revoke — create a new key instead.

API Reference

Server Actions

FunctionLocationPurpose
listApiKeys()features/api-keys/server/actions.tsList active (non-revoked) keys for the tenant
createApiKey(input)SameGenerate and store a new key, storing created_by
revokeApiKey(id)SameSoft-delete by setting revoked_at
validateApiKey(key)SameHash, look up, check expiry, return ApiKeyAuthContext

Permission Resolution

FunctionLocationPurpose
resolveApiKeyPermissions(ctx, membership?)features/api-keys/lib/permissions.tsCompute effective permissions for an authenticated key
lookupUserMembership(userId, tenantId)SameFetch creating user's roleId and role slug

Middleware

FunctionLocationPurpose
authenticateApiKey(request)features/api-keys/lib/middleware.tsExtract and validate key from request headers
unauthorizedResponse(message?)Same401 JSON response
forbiddenScopeResponse(scope)Same403 JSON response with required scope

Scope Utilities

FunctionLocationPurpose
normalizeApiScopes(scopes?)features/api-keys/lib/scopes.tsDefault to extraction:submit, collapse wildcards
hasScope(granted, required)SameCheck if granted scopes include the required one
expandScopePermissions(scopes)SameExpand scopes to Set<string> of permissions; returns null for *

Types

TypeLocationPurpose
ApiKeyfeatures/api-keys/types.tsFull key record (includes hash)
ApiKeyViewSameKey without hash (for API responses)
ApiKeyWithSecretSameView + plaintext secret (creation only)
ApiKeyAuthContextSameValidated key context (tenantId, scopes, createdBy, keyId)
CreateApiKeyInputSameCreation input shape
ApiScopefeatures/api-keys/lib/scopes.tsUnion type of all scope values
API_SCOPESSameConst array of all scope values
API_SCOPE_OPTIONSSameLabeled scope options for UI
SCOPE_TO_PERMISSIONSSameMap from scope to granular app permissions
UserMembershipfeatures/api-keys/lib/permissions.tsPre-fetched membership data (roleId, roleSlug)

Components

ComponentLocationPurpose
ApiKeyAdminfeatures/api-keys/components/api-key-admin.tsxFull admin CRUD UI with scope selection

For Agents

Agents do not manage API keys directly. However, API keys are the primary mechanism for external agents (including MCP tool servers) to interact with the platform:

  • External AI agents submit extraction results via POST /api/extraction/[entityId]/submit using a key with extraction:submit scope
  • CI/CD pipelines trigger extractions via keys with extraction:trigger scope
  • Custom integrations read and write records via keys with entities:read / entities:write scopes
  • MCP tool servers (e.g., Claude Code with the Amble MCP) authenticate with tools:execute scope or *

The key's effective permissions are bounded by the creating user's role. An owner should create keys intended for write operations; a guest-created key with entities:write scope will receive only the permissions guest members hold.

Design Decisions

Permission ceiling = creating user's role, not a configurable role. Earlier designs allowed a role field on the key itself, which was dropped in the 2026-03-28 schema sync. The replacement model is safer: the key cannot exceed what its creator was allowed to do, and admins cannot accidentally create keys more powerful than their own account. The role, assigned_to, and assigned_name fields remain on ApiKeyView (deprecated, hardcoded to "member" / null) for backward compatibility while consumer code migrates.

* scope means "all creator permissions", not "all system permissions". A wildcard key held by a viewer still cannot create records. This avoids a class of privilege-escalation bugs where issuing a *-scope key to a guest account would grant admin-level access.

Shared lookupUserMembership() eliminates the double DB query. Previous code fetched user membership independently in permission resolution and context building — two queries per request. The shared helper is called once and its result is threaded through both paths.

Fallback to ROLE_IDS.member for null createdBy. Keys created before created_by was tracked (prior to the schema sync) have no creator reference. Falling back to member permissions matches the behavior those keys had previously, so existing integrations are not disrupted.

  • Auth & Permissions (features/tenant/auth.ts) -- RBAC role and permission system shared with API key resolution; see Auth & Permissions
  • Extraction (features/entities/extraction/) -- external submission endpoint uses API key auth with extraction:submit scope
  • MCP (features/mcp/) -- Amble MCP tool server authenticates via API key; uses resolveApiKeyPermissions() for tool gating
  • Webhooks (features/webhooks/) -- external systems receiving webhooks often call back using API keys
  • Admin -- key management lives in Admin > API Keys

On this page