Skip to content

API Keys

API keys are the primary authentication mechanism for Ironflow’s API. Most /api/*, /ws, and ConnectRPC (/ironflow.v1.*) endpoints require a valid API key, with the exception of public endpoints: /health, /ready, /metrics, /api/v1/capabilities, /api/v1/auth/validate, /api/v1/auth/login, and /api/v1/platform/auth/login. Any OPTIONS request also bypasses auth, so CORS preflight works without a key.

One further route authenticates without an API key: POST /api/v1/webhooks/{id} accepts a per-source webhook ingest token (?token=ifwh_…, ADR 0048) so third-party providers that cannot send an Authorization header can deliver. An ifwh_ token is not an API key — it authenticates exactly one webhook source and carries no roles or scopes.

Unified Key Model

Ironflow uses a single API key type with RBAC-based authorization. All keys are stored in the same api_keys table and follow the same authentication path. What a key can do is determined entirely by its assigned roles, not by its type.

Keys are differentiated only by a cosmetic prefix:

PrefixPurposeTypical Roles
ifkey_Tenant operations (functions, events, runs, etc.)admin, developer, viewer
ifplatform_Platform/cross-tenant operationsplatform_admin, platform_operator, platform_viewer

Both prefixes go through the same auth middleware and are validated identically. The prefix is a visual hint for operators — it has no effect on authorization.

All key database IDs use the ak_ prefix (e.g., ak_550e8400-e29b-41d4-a716-446655440000). This is distinct from the display prefix shown in key listings, which uses the original ifkey_ or ifplatform_ prefix plus the first 8 hex characters (e.g., ifkey_a1b2c3d4).

Key Format

API keys use a prefix followed by 32 random hex characters (16 cryptographically random bytes):

ifkey_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 (38 chars, tenant)
ifplatform_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4 (43 chars, platform)

Keys are stored as SHA-256 hashes in the database — the raw key is never persisted. Once generated, a key can only be verified, not retrieved.

Creating Keys

Tenant Keys

Terminal window
# Key inherits the creating caller's environment when --env is omitted
ironflow apikey create my-key
# With specific roles
ironflow apikey create my-key --role role_abc123 --role role_def456
# Env-scoped key with expiration
ironflow apikey create my-key --env env_abc123 --expires 720h

Platform Keys

Platform keys authenticate cross-tenant operations (user management, tenant provisioning, impersonation).

Terminal window
ironflow apikey create my-platform-key --platform
Created Platform Key: my-platform-key (id: ak_h8i9j0k1)
Key: ifplatform_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Save this key — it will not be shown again.

Platform keys are assigned platform roles (platform_admin, platform_operator, platform_viewer) which control access to platform management endpoints.

The full key value is only returned once at creation time. Store it in a secrets manager.

Environment Scoping

Tenant API keys can optionally be scoped to a single environment. When an environment-scoped key authenticates, the server resolves the full hierarchy (environment → project → organization) and injects it into the request context for downstream authorization.

Terminal window
ironflow apikey create staging-key --env env_staging_abc123

Scoping rules:

  • One environment per key — A key can be scoped to at most one environment. It cannot be scoped to multiple environments.
  • Every tenant key is environment-scoped — When --env is omitted, handleCreateAPIKey stores the creating caller’s environment on the new key (internal/server/apikey_handlers.go). A tenant key that somehow carries no environment is rejected with 403 request has no environment scope on every request, and supplying X-Ironflow-Environment / ?env that differs from the key’s environment returns 403 environment does not match API key scope. The header/query fallback applies to JWT dashboard auth only.
  • Platform keys — Never environment-scoped. They use impersonation (X-Ironflow-Org header) to act on behalf of specific tenants.
  • Immutable scope — A key’s environment and organization cannot be changed after creation. To change scope, create a new key and delete the old one.

Authentication

HTTP Requests

Use the Authorization header with a Bearer prefix:

Terminal window
curl -H "Authorization: Bearer ifkey_a1b2c3d4..." http://localhost:9123/ironflow.v1.IronflowService/ListFunctions -H "Content-Type: application/json" -d '{}'

WebSocket Connections

WebSocket connections cannot always set custom headers. The Browser SDK sends credentials as base64url-encoded WebSocket subprotocol metadata and negotiates only the stable ironflow.v1 protocol. The credential is not placed in the URL or reflected in the upgrade response.

ironflow.configure({
serverUrl: "https://ironflow.example.com",
transport: "websocket",
auth: { token: session.accessToken },
});

The server still accepts ?token= from older and non-browser clients for compatibility, but new clients should use the Authorization header or the subprotocol mechanism. Query credentials can appear in server and proxy access logs.

SDK Authentication

SDKs pass credentials as a Bearer token in the Authorization header on HTTP requests. The Browser SDK uses the subprotocol mechanism above for WebSocket upgrades.

client := ironflow.NewClient(ironflow.ClientConfig{
ServerURL: "http://localhost:9123",
APIKey: "ifkey_a1b2c3d4...",
})

Dashboard Authentication

The web dashboard uses JWT cookies for authentication, not API keys. Users log in via /api/v1/auth/login and receive an ironflow_dashboard_token cookie (24-hour TTL). The auth middleware tries API key auth first; if no key is present, it falls back to the JWT cookie.

Validation Flow

The auth middleware runs on every non-public request. Here is the full validation sequence:

sequenceDiagram
    participant Client
    participant Middleware as Auth Middleware
    participant Store as Database
    participant RBAC as RBAC Evaluator
    participant Audit as Audit Emitter
    participant Handler as Route Handler

    Client->>Middleware: HTTP request

    Note over Middleware: Check if public path<br/>(/health, /ready, /metrics, etc.)
    alt Public path
        Middleware->>Handler: Pass through (no auth)
    else Protected path
        Middleware->>Middleware: Extract API key from<br/>Authorization header or ?token param

        alt API key present
            Middleware->>Store: Lookup key by SHA-256 hash
            alt Key not found
                Middleware-->>Client: 401 Invalid API key
            else Key found
                Middleware->>Middleware: Check expiration
                alt Key expired
                    Middleware-->>Client: 401 API key expired
                else Key valid
                    Middleware->>Store: Load roles via api_key_roles
                    Middleware->>Middleware: Resolve project from environment
                    Middleware->>RBAC: Evaluate policy (roles, action, resource)
                    Middleware->>Audit: Emit authz decision (async)
                    alt Denied
                        Middleware-->>Client: 403 Insufficient permissions
                    else Allowed
                        Middleware->>Store: Update last_used_at (async)
                        Middleware->>Middleware: Build RequestContext<br/>(org, project, env, roles)
                        alt Platform key with X-Ironflow-Org header
                            Middleware->>Middleware: Handle impersonation flow
                        end
                        Middleware->>Handler: Forward with RequestContext
                        Handler-->>Client: Response
                    end
                end
            end
        else No API key
            Middleware->>Middleware: Check for JWT dashboard cookie
            alt Valid JWT
                Middleware->>Handler: Forward with JWT context
                Handler-->>Client: Response
            else No valid auth
                Middleware-->>Client: 401 Authentication required
            end
        end
    end

Key Lifecycle

Rotation

Rotation creates a new key (new ID, new raw value) while preserving the name, environment, expiration, and role assignments from the old key. The old key is deleted atomically.

Terminal window
ironflow apikey rotate ak_abc123

Rotation produces a new key ID (ak_*) and a new raw key value (ifkey_* / ifplatform_*). The raw key is only printed once — save it immediately. If your systems reference the old key ID, update them along with the raw key value.

Expiration

Keys can be created with an optional expiration duration:

Terminal window
ironflow apikey create short-lived-key --expires 720h # 30 days
ironflow apikey create ci-key --expires 24h # 1 day

The expiration is enforced at authentication time — any request with an expired key receives a 401 API key expired response. Expired keys cannot be rotated; delete them and create a new key instead.

Deletion

Terminal window
ironflow apikey delete ak_abc123

The key is immediately revoked. Any requests using this key will start receiving 401 errors.

Listing Keys

Terminal window
# List all tenant keys
ironflow apikey list
# List platform keys
ironflow apikey list --platform
# JSON output
ironflow apikey list --json
ID NAME PREFIX ROLES CREATED LAST USED EXPIRES
ak_abc123 my-key ifkey_a1b2c3d4 role_abc123 2026-01-01T00:00:00Z 2026-03-01T12:00:00Z -

The full key value is never shown in list output — only the display prefix (ifkey_ or ifplatform_ + first 8 hex chars) for identification.

Bootstrap Key

On first boot, Ironflow generates a default admin API key (ID: ak_admin_bootstrap) with the admin role. The plaintext key is written to a file (mode 0400) and the server prints the file path; it is never written to stdout or journalctl. On subsequent restarts, only the key prefix is shown.

The file path is controlled by --bootstrap-key-file:

DeploymentDefaultNotes
Single binary / dev<db-dir>/.ironflow_bootstrap_key.jsonSame dir as the SQLite database.
PostgreSQL (no --bootstrap-key-file)<os.TempDir>/.ironflow_bootstrap_key.jsonFallback whenever the storage driver is Postgres or no database path is set. Host-global and a fixed name, so two serve processes on one box share it — pass --bootstrap-key-file explicitly in multi-node deployments.
Meta-cluster systemd/run/ironflow/bootstrap-key.jsonSet explicitly by the unit; tmpfs (auto-clears on reboot, never persisted to disk).

Read the key with:

Terminal window
cat /path/to/bootstrap-key.json | jq -r .key

The file contains JSON: {"key":"…","key_id":"ak_admin_bootstrap","org_id":"org_default","role":"admin","dashboard_password":"…","timestamp":"…"}.

dashboard_password is the one-time dashboard admin password for admin@ironflow.local (ADR 0072). It rides the same file so it is not printed to stdout — on a container that stdout is the json-file log, where a working credential would outlive the deploy. Read it with:

Terminal window
cat /path/to/bootstrap-key.json | jq -r .dashboard_password

The field is omitted when that boot issued a key but reused an existing dashboard user, and by any server released before ADR 0072 landed. Treat absence as “not here”, not as an error. When the file carries no password, the server prints the plaintext in its startup banner instead — stdout is then the only channel.

Read it once and delete the file, the same as the key. A reader that keeps the file must strip the field instead: Ironflow Desktop re-reads this file on every session, so it removes dashboard_password on first read rather than leaving a second live admin credential in the app-data directory.

Read, delete, rotate

Read the file once, delete it immediately (rm -f /path/to/bootstrap-key.json), then rotate via ironflow apikey rotate ak_admin_bootstrap and change the dashboard password. Do not leave the file on disk — anything that can read the file as the ironflow user can take over the meta-cluster. The structured log event BOOTSTRAP_ADMIN_KEY_ISSUED carries the file path (file_path field) but never the key or the password, so central log shipping is safe to enable.

If the file write fails (read-only fs, ENOSPC, etc.), no admin key is issued at all. Since the key file is written before the ak_admin_bootstrap row is inserted, a plaintext that cannot be made durable never gets a row that would accept it. The server logs BOOTSTRAP_ADMIN_KEY_ISSUED with a file_path_error field, prints the same in its startup banner, and issues nothing. There is nothing to reset: fix the --bootstrap-key-file path and restart, and the next boot issues a key normally.

One caveat on that boot: the dashboard admin user is created before the key, but its admin role is assigned after — deliberately, so an unissued key leaves bootstrap incomplete and the next boot retries it. The password printed in that boot’s banner is live, but the account carries no role until the restart succeeds.


Roles

Keys derive all their permissions from assigned roles. See Roles & Permissions for the full permissions matrix.

Tenant roles (for ifkey_ keys):

RoleDescription
adminFull access — all read, write, and delete operations
developerRead and write access — can create and modify resources
viewerRead-only access — can list and read resources

Platform roles (for ifplatform_ keys):

RoleDescription
platform_adminFull access to all platform operations (wildcard)
platform_operatorTenant management, impersonation, read access to users/keys/roles
platform_viewerRead-only access to all platform resources

Custom roles can be created for fine-grained access control.


FAQ

Can a key be scoped to multiple environments?

No. A key can be scoped to at most one environment. Create separate keys per environment — no key can span environments.

Can I change a key’s environment or organization after creation?

No. A key’s scope (org, environment) and name are immutable. There is no update endpoint for keys. To change scope, create a new key and delete the old one. This prevents a distributed key from silently gaining or losing access.

What can I change on an existing key?

Only role assignments can be modified (PUT /api/v1/apikeys/{id}/roles). Everything else — name, environment, organization — is fixed for the lifetime of the key.

What’s the difference between omitting --env and using --env?

  • Omit --env: the key inherits the environment of the caller that created it. Sending X-Ironflow-Environment (or ?env) for a different environment returns 403 environment does not match API key scope.
  • Use --env env_id: env-scoped key. The key is locked to that environment permanently.

How do I move a key from one environment to another?

You can’t. Delete the old key and create a new one scoped to the new environment.

What happens to expired keys?

Expired keys return 401 API key expired on every request. They cannot be rotated — delete and recreate. Expired keys remain in listings until explicitly deleted.