Files
optimclaw/docs/USER_MANAGEMENT_API.md
T

9.2 KiB

User Management API

DB-backed user management for multi-tenant IronClaw deployments. Covers admin user CRUD, self-service profile, API token management, and usage reporting.

Authentication

All endpoints require Authorization: Bearer <token>. Tokens are either:

  • Env-var tokens — configured via GATEWAY_AUTH_TOKEN (single-user) at startup
  • DB-backed tokens — created via POST /api/tokens or POST /api/admin/users

DB tokens are SHA-256 hashed at rest; plaintext is returned exactly once at creation time.

Auth is cached in a bounded LRU (1024 entries, 60s TTL). Suspending a user or revoking a token may take up to 60s to take effect.

Roles

Role Scope
admin Full access to all endpoints
member Self-service profile + own token management only

Endpoints marked Admin return 403 Forbidden for member role.


Admin: Users

POST /api/admin/users

Create a new user. Returns the user record and a one-time plaintext API token.

Auth: Admin

Request body:

{
  "display_name": "Alice Smith",
  "email": "[email protected]",
  "role": "member"
}
Field Type Required Default Notes
display_name string yes
email string no null Must be unique if provided
role string no "member" "admin" or "member"

Response: 200 OK

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "[email protected]",
  "display_name": "Alice Smith",
  "status": "active",
  "role": "member",
  "token": "a1b2c3d4e5f6...64-char hex...",
  "created_at": "2026-03-25T12:00:00+00:00",
  "created_by": "admin-user-id"
}

The token field is the plaintext API token. It is shown only once — store it securely.

Errors: 400 (missing display_name, invalid role), 403 (not admin), 503 (no database)


GET /api/admin/users

List all users.

Auth: Admin

Response: 200 OK

{
  "users": [
    {
      "id": "550e8400-...",
      "email": "[email protected]",
      "display_name": "Alice Smith",
      "status": "active",
      "role": "member",
      "created_at": "2026-03-25T12:00:00+00:00",
      "updated_at": "2026-03-25T12:00:00+00:00",
      "last_login_at": "2026-03-25T14:30:00+00:00",
      "created_by": "admin-user-id"
    }
  ]
}

GET /api/admin/users/{id}

Get a single user by ID.

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "email": "[email protected]",
  "display_name": "Alice Smith",
  "status": "active",
  "role": "member",
  "created_at": "2026-03-25T12:00:00+00:00",
  "updated_at": "2026-03-25T12:00:00+00:00",
  "last_login_at": "2026-03-25T14:30:00+00:00",
  "created_by": "admin-user-id",
  "metadata": {}
}

Errors: 404 (user not found), 403 (not admin)


PATCH /api/admin/users/{id}

Update a user's display name and/or metadata. Omitted fields are left unchanged.

Auth: Admin

Request body:

{
  "display_name": "Alice Johnson",
  "metadata": {"department": "engineering"}
}
Field Type Required Notes
display_name string no
metadata object no Replaces entire metadata object (merge patch)

Response: 200 OK — returns the full updated user record (same shape as GET detail, without last_login_at/created_by).

Errors: 404 (user not found), 403 (not admin)


POST /api/admin/users/{id}/suspend

Suspend a user. Suspended users cannot authenticate (DB auth checks user status).

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "status": "suspended"
}

Errors: 404 (user not found), 403 (not admin)


POST /api/admin/users/{id}/activate

Re-activate a suspended user.

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "status": "active"
}

Errors: 404 (user not found), 403 (not admin)


DELETE /api/admin/users/{id}

Permanently delete a user and all associated data (tokens, jobs, conversations, memory, routines, settings, secrets).

Auth: Admin

Response: 200 OK

{
  "id": "550e8400-...",
  "deleted": true
}

Errors: 404 (user not found), 403 (not admin)

Cascade: Deletes from api_tokens, agent_jobs, conversations, memory_documents, routines, secrets, settings, wasm_tools, and related tables. On PostgreSQL this uses FK cascades; on libSQL it uses explicit deletes.


Admin: Usage

GET /api/admin/usage

Per-user LLM usage statistics aggregated from llm_calls via agent_jobs.user_id.

Auth: Admin

Query parameters:

Param Type Default Notes
user_id string all users Filter to a single user
period string "day" "day" (24h), "week" (7d), or "month" (30d)

Response: 200 OK

{
  "period": "week",
  "since": "2026-03-18T12:00:00+00:00",
  "usage": [
    {
      "user_id": "alice-id",
      "model": "claude-sonnet-4-5-20250514",
      "call_count": 42,
      "input_tokens": 150000,
      "output_tokens": 30000,
      "total_cost": "1.23"
    }
  ]
}

Self-Service: Profile

GET /api/profile

Get the authenticated user's own profile.

Auth: Any authenticated user

Response: 200 OK

{
  "id": "550e8400-...",
  "email": "[email protected]",
  "display_name": "Alice Smith",
  "status": "active",
  "role": "member",
  "created_at": "2026-03-25T12:00:00+00:00",
  "last_login_at": "2026-03-25T14:30:00+00:00"
}

PATCH /api/profile

Update the authenticated user's own display name and/or metadata.

Auth: Any authenticated user

Request body:

{
  "display_name": "Alice Johnson",
  "metadata": {"theme": "dark"}
}

Response: 200 OK

{
  "id": "550e8400-...",
  "display_name": "Alice Johnson",
  "updated": true
}

Self-Service: Tokens

POST /api/tokens

Create a new API token for the authenticated user. Admins can optionally create tokens for other users by including user_id.

Auth: Any authenticated user

Request body:

{
  "name": "CI pipeline",
  "expires_in_days": 90,
  "user_id": "other-user-id"
}
Field Type Required Notes
name string yes Human-readable label
expires_in_days integer no null = never expires
user_id string no Admin-only; create token for another user

Response: 200 OK

{
  "token": "a1b2c3d4...64-char hex...",
  "id": "token-uuid",
  "name": "CI pipeline",
  "token_prefix": "a1b2c3d4",
  "expires_at": "2026-06-23T12:00:00+00:00",
  "created_at": "2026-03-25T12:00:00+00:00"
}

The token field is shown only once.


GET /api/tokens

List the authenticated user's tokens. Token hashes are never returned.

Auth: Any authenticated user

Response: 200 OK

{
  "tokens": [
    {
      "id": "token-uuid",
      "name": "CI pipeline",
      "token_prefix": "a1b2c3d4",
      "expires_at": "2026-06-23T12:00:00+00:00",
      "last_used_at": "2026-03-25T14:00:00+00:00",
      "created_at": "2026-03-25T12:00:00+00:00",
      "revoked_at": null
    }
  ]
}

DELETE /api/tokens/{id}

Revoke one of the authenticated user's tokens. Users can only revoke their own tokens.

Auth: Any authenticated user

Path: id — UUID of the token to revoke

Response: 200 OK

{
  "status": "revoked",
  "id": "token-uuid"
}

Errors: 400 (invalid UUID), 404 (token not found or belongs to another user)


Error Format

All error responses return a plain text body with the error message and the corresponding HTTP status code:

Code Meaning
400 Bad request (missing fields, invalid input)
401 Missing or invalid bearer token
403 Authenticated but insufficient role (member accessing admin endpoint)
404 Resource not found
503 Database not available
500 Internal server error

Database Schema

users

Column Type (PG / libSQL) Notes
id UUID / TEXT Primary key, UUID v4
email TEXT UNIQUE Nullable
display_name TEXT NOT NULL
status TEXT NOT NULL "active" or "suspended"
role TEXT NOT NULL "admin" or "member"
created_at TIMESTAMPTZ / TEXT
updated_at TIMESTAMPTZ / TEXT
last_login_at TIMESTAMPTZ / TEXT Nullable
created_by TEXT Nullable, references users.id
metadata JSONB / TEXT Default {}

api_tokens

Column Type (PG / libSQL) Notes
id UUID / TEXT Primary key
user_id TEXT NOT NULL FK to users.id (PG cascades; libSQL uses explicit cleanup)
token_hash BYTEA / BLOB SHA-256 of hex-encoded plaintext
token_prefix TEXT NOT NULL First 8 chars for identification
name TEXT NOT NULL Human-readable label
expires_at TIMESTAMPTZ / TEXT Nullable
last_used_at TIMESTAMPTZ / TEXT Nullable
created_at TIMESTAMPTZ / TEXT
revoked_at TIMESTAMPTZ / TEXT Nullable; set on revocation