- Deploy & Operate
- Platform Architecture
Platform Architecture
The platform layer adds cross-tenant management capabilities on top of Ironflow’s multi-tenant architecture. This document explains how the system works under the hood.
Platform vs Tenant Identity
Section titled “Platform vs Tenant Identity”Ironflow has two distinct identity domains:
All API keys (both tenant and platform) are stored in the unified api_keys table with ak_ ID prefix. The key value prefix (ifkey_ vs ifplatform_) is a cosmetic distinction. Platform roles are stored in the unified roles table.
Platform identity is global — a platform user or key can manage any tenant. Platform credentials are scoped to org_platform, a sentinel organization that exists solely to anchor platform resources in the database.
Tenant identity is organization-scoped — a tenant API key can only access resources within its own organization. Within an organization, resources are further organized into Projects, and each project contains one or more Environments. The hierarchy is: Organization → Project → Environment → Resources.
Authentication Flow
Section titled “Authentication Flow”Platform authentication supports two methods:
Email/Password → JWT
Section titled “Email/Password → JWT”The JWT payload includes a boolean platform claim ("platform": true), which the auth middleware uses to distinguish platform tokens from tenant tokens. See internal/server/jwt.go.
API Key
Section titled “API Key”Platform API keys use the ifplatform_ prefix (vs ifkey_ for tenant keys). Both go through the same unified auth middleware:
- Extract the credential from the
Authorization: Bearer <key>header. The middleware tries it as an engine JWT first and treats it as an API key only if it is not a valid JWT - SHA-256 hash the key, look up in the unified
api_keystable - Load assigned roles from
api_key_rolesjunction table - If the key belongs to
org_platform, setRequestContext.IsPlatform = true
Impersonation Model
Section titled “Impersonation Model”When a platform token includes the X-Ironflow-Org header, the request is “impersonated” — the platform identity acts within a tenant’s context:
Without the X-Ironflow-Org header, platform tokens are restricted to /api/v1/platform/* routes only. Any attempt to access tenant endpoints (like /ironflow.v1.IronflowService/ListFunctions) without impersonation returns 403.
A browser cannot set a header on a WebSocket, so the /ws upgrade also takes the org from the org_id query parameter. No other route reads it. Only a platform principal impersonates: on an authenticated route, a tenant credential that names another org gets 403.
RBAC Model
Section titled “RBAC Model”Platform RBAC uses the same architecture as tenant RBAC but with its own action namespace:
Platform routes are protected by the RequirePlatform guard middleware in internal/platform/guard.go. The embedded Rego engine and the policy-set API ship in the binary unconditionally (no license gate). System RBAC (Layer 1) is authoritative for ALLOW; the Rego policy set (Layer 2) is subtractive deny-only by design — and Layer 2 enforcement is opt-in. The active evaluator returned by rbac.Evaluator() is the L1-only RBACEvaluator unless spec.auth.policyEnforcement is true (or IRONFLOW_POLICY_ENFORCEMENT=true without a config file). In that case internal/server/server.go installs the layered evaluator through rbac.SetEvaluator(). See Authorization policy sets, ADR 0016 and ADR 0121.
The three built-in platform roles (role_platform_admin, role_platform_operator, role_platform_viewer) ship with hardcoded permissions. platform_admin uses a wildcard (*) that matches all actions. Custom roles hold no built-in permissions. Deny rules for them live in the policy set.
Audit Trail
Section titled “Audit Trail”Platform operations generate two types of audit records:
Platform Events
Section titled “Platform Events”Handler-level events for resource lifecycle:
| Event | Trigger |
|---|---|
platform.user.created/updated/deleted | User CRUD operations |
platform.key.created/revoked | Reserved in store.AuditEventType; no handler emits them today |
platform.role.changed | A blocked delete or update of a built-in platform role |
platform.impersonated | Any impersonated request |
Authorization Decisions
Section titled “Authorization Decisions”The auth middleware logs every allow/deny decision with the full context:
- Platform user/key ID
- Requested action
- Impersonated org (if applicable)
- Result (allow/deny)
Recording
Section titled “Recording”Audit events are emitted asynchronously (go authaudit.Emitter().Emit*(...)) using context.WithoutCancel() to avoid cancellation from the HTTP request lifecycle. Events are stored in the audit_events table with scope = "platform".
Data Model
Section titled “Data Model”Platform resources are stored in unified tables alongside tenant resources, differentiated by org_id = org_platform.
platform_users ├── id (puser_*) ├── email (unique) ├── name ├── password_hash ├── is_active └── last_login_at
roles (unified — platform roles have org_id = org_platform) ├── id (role_platform_admin, role_platform_operator, role_platform_viewer) ├── org_id ├── name (unique per org) └── is_default (built-in flag)
policy_sets (one row per organization; the platform set has org_id = org_platform) ├── org_id (primary key) ├── version ├── modules (file name → Rego source) ├── decision (optional rule path) ├── mode (enforce or monitor) └── updated_at, updated_by
policy_set_versions (the newest 100 versions of each set) └── org_id, version, modules, decision, mode, created_at, created_by
user_roles (unified) ├── user_id → platform_users or users └── role_id → roles
api_key_roles (unified) ├── api_key_id → api_keys └── role_id → roles
api_keys (unified — platform keys have org_id = org_platform) ├── id (ak_*) ├── key_hash (SHA-256) ├── key_prefix (ifplatform_* or ifkey_*) └── org_id
audit_events (scope=platform) ├── id (ULID-shaped TEXT primary key) ├── environment_id ├── run_id ├── function_id ├── step_id ├── event_type (free TEXT — the CHECK constraint was dropped by the │ audit_drop_event_type_check migration (postgres 050, │ sqlite 052); store.AuditEventType in Go is the source │ of truth) ├── payload (JSON) ├── metadata (JSON, nullable) ├── scope ('tenant' | 'platform', default 'tenant') ├── platform_key_id (nullable) ├── platform_user_id (nullable) ├── impersonated_org_id (nullable) └── created_atNext Steps
Section titled “Next Steps”- Managing Platform Users & Keys — create and manage platform credentials
- Impersonating Tenants — cross-tenant operations
- Platform RBAC — roles and permissions
- Platform API Reference — full endpoint documentation