Skip to content

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.


Ironflow has two distinct identity domains:

Platform vs Tenant Identity — two-tier scope hierarchy showing platform-level and tenant-level resources

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.


Platform authentication supports two methods:

Platform Authentication Flow — email/password to JWT token with platform scope

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.

Platform API keys use the ifplatform_ prefix (vs ifkey_ for tenant keys). Both go through the same unified auth middleware:

  1. Extract key from Authorization: Bearer <key> header
  2. SHA-256 hash the key, look up in the unified api_keys table
  3. Load assigned roles from api_key_roles junction table
  4. If the key belongs to org_platform, set RequestContext.IsPlatform = true

When a platform token includes the X-Ironflow-Org header, the request is “impersonated” — the platform identity acts within a tenant’s context:

Impersonation Model — platform token with org header flows through validation, context overlay, and audit

Without the X-Ironflow-Org header, platform tokens are restricted to /api/v1/platform/* routes only. Any attempt to access tenant endpoints (like /api/v1/functions) without impersonation returns 403.


Platform RBAC uses the same architecture as tenant RBAC but with its own action namespace:

Platform RBAC Flow — request to action derivation, policy evaluation with core/enterprise branching, allow or deny

Platform routes are protected by the RequirePlatform guard middleware in internal/platform/guard.go. CEL evaluation is wired natively at startup: rbaccel.NewEvaluator() is constructed in internal/server/policy_handlers.go and composed with the built-in RBAC evaluator via rbac.NewLayeredEvaluator() (internal/auth/rbac/evaluator.go). System RBAC (Layer 1) is authoritative for ALLOW; tenant-edited CEL (Layer 2) is subtractive deny-only. There is no license gate — all RBAC + CEL functionality is available unconditionally.

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 require explicit action grants via policies.


Platform operations generate two types of audit records:

Handler-level events for resource lifecycle:

EventTrigger
platform.user.created/updated/deletedUser CRUD operations
platform.key.created/revokedKey creation, rotation, deletion
platform.role.changedRole create/update/delete
platform.impersonatedAny impersonated request

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)

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".


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)
policies (unified — platform policies have org_id = org_platform)
├── id (pol_*)
├── org_id
├── name, effect, actions, resources, condition
└── (attached to roles via role_policies junction table)
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 (enum — see schema for full list)
├── payload (JSON)
├── metadata (JSON, nullable)
├── scope ('tenant' | 'platform', default 'tenant')
├── platform_key_id (nullable)
├── platform_user_id (nullable)
├── impersonated_org_id (nullable)
└── created_at