Skip to main content

OpenFGA Authorization Design

Status

Proposed high-level design.

This document defines the target authorization architecture for the Control Plane, Keycloak, OpenFGA, the Cluster Agent, and downstream services. It also records the gaps between the current platform and that target, and the work required to close them.

Executive Summary

The platform will use OpenFGA as a derived authorization graph for resources inside a workspace. The Control Plane remains the source of truth for accounts, workspaces, principals, WorkspaceMember records, workspace-local role definitions, and WorkspaceMemberRole assignments. Each downstream service remains the source of truth for its own resources, resource hierarchy, and direct resource grants.

Each workspace has one OpenFGA store in the tenant cluster. The store contains:

  • exactly one lightweight workspace:<workspace-uid> relationship root;
  • workspace membership, first-class workspace-role objects, role members, and role action permissions;
  • downstream resource-to-workspace and resource-to-parent relationships; and
  • direct grants on downstream resources.

Every five minutes, the Cluster Agent fetches the complete workspace role and membership snapshot exclusively through the Control Plane API and synchronizes the Control Plane-owned tuples in the workspace store. It never reads the Control Plane database directly. This full-snapshot approach is intentionally simple because a workspace is expected to have fewer than 100 role and membership records. Downstream services synchronize their own resource and grant tuples. Every tuple category has exactly one owning system and can be rebuilt from that owner's primary data.

Backend services authorize a request by validating the Keycloak token, resolving it to a canonical Control Plane principal, and asking OpenFGA whether that principal has the required permission on the target object. Keycloak role claims may be used to improve the UI, but are not authoritative for backend authorization.

Goals

  • Define a common relationship model across accounts, workspaces, roles, principals, and downstream resources.
  • Make ownership and source-of-truth boundaries explicit.
  • Support people and machine principals using stable, non-personal identifiers.
  • Inherit workspace permissions into downstream resources without duplicating a tuple per resource.
  • Support user-defined workspace roles while retaining the eight Control Plane preset roles.
  • Support direct grants to principals or workspace roles without moving resource ownership to the Control Plane.
  • Define provisioning, periodic synchronization and repair, revocation, and failure behavior.
  • Allow downstream teams to add resource types without inventing a separate authorization architecture.
  • Produce an actionable implementation backlog for every participating system.

Non-goals

  • Replacing Keycloak as the authentication provider.
  • Storing profiles, resource metadata, or searchable domain data in OpenFGA.
  • Completing every preset-role action mapping before the first rollout. Actions migrate incrementally as each product path adopts OpenFGA.
  • Using OpenFGA to authorize the OpenFGA administration API itself. Administrative access is controlled by deployment credentials and network policy.

Design Decisions

One OpenFGA store per workspace

Each workspace has one OpenFGA store, matching the current provisioning and tenant-isolation boundary. The store contains only that workspace's authorization tuples; the sync worker rejects any Control Plane snapshot whose workspace UID does not match the store configuration.

Because an OpenFGA model cannot reference the store itself, each store uses one workspace:<workspace-uid> object as the graph root for workspace membership, roles, permissions, and top-level downstream resources. Account data and Control Plane API authorization are not represented in the store.

OpenFGA is a projection, not the system of record

OpenFGA is optimized for relationship checks, not for profiles, resource discovery, or domain transactions. The Control Plane and downstream databases retain their authoritative records, while OpenFGA contains the minimum graph needed for authorization. This allows a store to be rebuilt and makes periodic synchronization deterministic. This follows OpenFGA's source-of-truth guidance.

Roles are first-class workspace objects

Control Plane roles are modeled as workspace_role:<workspace-role-uid> objects. A WorkspaceMemberRole row makes a principal a member of the role:

workspace_role:<role-uid>#workspace@workspace:<workspace-uid>
workspace_role:<role-uid>#member@principal:<principal-uid>

The role's display name is Control Plane data and is not used as an OpenFGA identifier or relation name. Consequently, a role may be created, renamed, or deleted without changing the authorization model.

The derived active_member userset intersects workspace_role#member with workspace#member. Role permissions and downstream grants target active_member, so removing the workspace membership makes any stale role assignment ineffective.

OpenFGA relations on workspace represent stable, versioned action permissions rather than role names. The Control Plane maps each role to its configured actions by writing the role's active-member userset to those relations:

workspace:<workspace-uid>#workflow_can_edit@workspace_role:<role-uid>#active_member
workspace:<workspace-uid>#workflow_can_read@workspace_role:<role-uid>#active_member

Downstream models inherit these action permissions through their workspace parent. Adding or removing an existing action from a role is a tuple change. Introducing or renaming an action relation is an immutable OpenFGA model change.

Each workspace is seeded with these eight presets:

Preset keyDescription
platform_adminManage users, assign roles, manage policies, view all assets.
data_engineerBuild ETL pipelines, manage Delta tables, schedule jobs.
ml_engineerTrain & deploy models, manage MLflow runs, feature engineering.
data_analystQuery datasets, build dashboards, limited notebook access.
business_userView curated dashboards and reports.
data_stewardTag, classify, and curate datasets, manage metadata/lineage.
tenant_adminManage tenant configurations, users, and shared resources.
support_userHas full administrative control over the entire platform. Can manage all accounts, users, global settings, and billing at the platform level.

They use the same membership and action-permission mechanism as user-defined roles. Preset keys are stable across workspaces, but role UIDs may differ because existing records are migrated. Custom display names are never converted into relation names.

Every tuple category has one authoritative owner

The Control Plane owns scope and membership tuples. A downstream service owns its resource types, parent edges, and direct grants. Multiple replicas of an owner's synchronization worker may write idempotently, but no other system may create, delete, or repair that owner's tuples.

OpenFGA authentication alone does not establish type-level tuple ownership. The platform must enforce write boundaries through a tuple-writer gateway or equivalent scoped adapter, separate credentials, and network policy. Direct unrestricted write access to the store is not part of the downstream contract.

Architecture

OpenFGA is the center of authorization evaluation, but not the source of truth for either side. The Control Plane owns identities, role definitions, and scope memberships. Every downstream microservice owns its access-controlled objects and direct grants. Both sides project only their owned relationships into OpenFGA, while each downstream backend consults OpenFGA before returning resources or performing a protected action. Workflow is the first proof of concept for this repeated integration pattern, not a special authorization component.

Request path

  1. Keycloak authenticates a person or machine.
  2. The BFF or downstream service validates the request and resolves it to a canonical Control Plane principal UID.
  3. The service loads the requested resource from its database and verifies that it belongs to the current workspace. A missing resource returns 404 before authorization details are exposed.
  4. The service sends an OpenFGA check using the canonical principal, permission, object, store ID, and pinned authorization model ID.
  5. The service performs the operation only when OpenFGA returns allowed: true.

The UI may use role claims to hide unavailable features, but the UI is not an enforcement point. Every backend operation performs its own check.

Trust boundaries

  • OpenFGA is private to the workspace network and is never exposed to browsers.
  • Only the authorization model deployer may create stores or write models.
  • Check clients receive read/check access only.
  • Tuple writers authenticate as a specific owner and may mutate only the relations and object types assigned to that owner.
  • Cluster Agent credentials are bound to one workspace. The Control Plane roles-and-memberships endpoint rejects cross-workspace access.
  • The authorization sync worker has no Control Plane database credentials or direct database network path. It reads the complete snapshot through the Control Plane API.
  • Store ID and model ID are deployment configuration, not request parameters accepted from an end user.

System Ownership

Data or processSource of truthOpenFGA representationSynchronization owner
People and machine principalsControl Plane; Keycloak owns authentication credentialsprincipal:<control-plane-principal-id>None required merely for existence
AccountsControl PlaneNot representedNone
WorkspacesControl PlaneExactly one workspace:<workspace-uid> relationship root per storeControl Plane authorization sync worker
Workspace role definitionsControl Plane WorkspaceRole recordsworkspace_role:<role-uid> with a workspace parentControl Plane authorization sync worker
Preset role definitionsControl Plane seed and migration logicThe same workspace_role representation as custom rolesControl Plane authorization sync worker
Registered action permissionsVersioned platform and product authorization contractWritable action relations on workspace and downstream resource typesModel deployment pipeline
Role action permissionsControl Plane role configurationworkspace_role#active_member userset on each configured workspace actionControl Plane authorization sync worker
Workspace membershipControl Plane WorkspaceMemberPrincipal-to-workspace#member tupleControl Plane authorization sync worker
Workspace role assignmentControl Plane WorkspaceMemberRolePrincipal-to-workspace_role#member tupleControl Plane authorization sync worker
Downstream resourcesOwning service databaseTyped object and parent relationshipOwning service authorization sync worker
Resource hierarchyOwning service databaseParent relation, such as workflow#projectOwning service authorization sync worker
Direct resource grantsOwning service databasePrincipal- or role-userset-to-resource tupleOwning service authorization sync worker
OpenFGA store lifecyclePlatform provisioningOne store per workspacePlatform provisioner
Authorization model sourcePlatform authorization repository, with service-owned modulesImmutable model version in the storeModel deployment pipeline
Store ID and active model IDWorkspace deployment configurationUsed on every API callPlatform operations

OpenFGA never owns user profile data, display names, resource state, or resource search indexes. Direct grants are also persisted by the downstream service so they can be audited, displayed, and reconstructed.

Identity and Membership Mapping

Canonical principal identifiers

All people and machines use an immutable Control Plane principal ID:

principal:<control-plane-principal-id>

The ID must be an opaque UUID or ULID generated by the Control Plane. It must not contain an email address, username, Keycloak client ID, realm name, or other mutable or personal value. The Control Plane records the principal kind (person or machine) and lifecycle state as metadata; both kinds use the same OpenFGA type so authorization models do not need duplicate relations.

Deleting a principal does not permit reuse of its ID. A replacement identity receives a new principal ID.

Before calling OpenFGA, the BFF or downstream service resolves the authenticated identity to an immutable Control Plane principal UID. Authentication and claim mapping are outside this design. Email addresses, usernames, and Keycloak role claims are not used as OpenFGA identifiers or authorization decisions.

Workspace membership

Account association remains internal to the Control Plane and creates no OpenFGA tuple. It does not grant access to a workspace or a downstream resource.

Every Control Plane role assignment belongs to a WorkspaceMember through WorkspaceMemberRole. This includes roles whose names or descriptions sound platform-wide, such as the platform_admin, tenant_admin, and support_user presets; in this design they apply only to the workspace of that membership. Access to another workspace requires another WorkspaceMember and another role assignment.

Every active WorkspaceMember produces a structural workspace#member tuple, independently of its assigned roles. The Control Plane maintains the invariant that a WorkspaceMemberRole can reference only a member and a role from the same workspace. Product services do not infer workspace membership from a role assignment.

The Control Plane must populate Principal.workspaceMembers from the authoritative workspace-membership table. It must not calculate this field from OpenFGA.

Role scopes

ScopeExamplesOwnerRepresentation
Workspace membership roleEight seeded presets plus user-defined rolesControl Plane WorkspaceRole; platform and product teams register the available action-permission keysworkspace_role:<role-uid> and one tuple for each action assigned to the role

There are no account-scoped role assignments in the Control Plane or OpenFGA. There are also no downstream resource roles: access is granted directly to actions such as can_read, can_edit, or can_delete. The platform_admin and tenant_admin presets include both administrative and data access.

Preset-role actions migrate incrementally. Existing frontend preset-role logic remains temporarily for actions that have not migrated. After an action migrates, OpenFGA becomes authoritative and the frontend asks the BFF or backend whether the principal can perform that action instead of maintaining its own role mapping.

All Control Plane roles in scope are workspace-membership roles: every WorkspaceRole belongs to exactly one workspace, and principals receive it only through WorkspaceMemberRole. There is no separate global, platform, tenant, or account role assignment type; names such as platform_admin and tenant_admin are preset names whose authorization scope is still one workspace.

Whether a machine principal may receive a particular workspace role is a Control Plane policy decision and is outside this design.

Control Plane schema mapping

The current Prisma schema supplies the assignment structure required by this design:

  • WorkspaceMember is unique for (workspaceId, principalId) and represents the principal's membership in one workspace;
  • WorkspaceMemberRole joins one WorkspaceMember to one workspace role and is unique for (workspaceMemberId, workspaceRoleId); and
  • the stable uid values, not database BigInt keys or mutable role names, are used in APIs and OpenFGA object identifiers.

Role configuration must additionally expose whether a role is a protected preset or user-defined and which registered action-permission keys it contains. If that metadata is not present in the evolving Control Plane schema, it is required implementation work rather than something inferred by the sync worker.

Assignment and revocation

Role creation or update is a Control Plane transaction that validates the workspace-local name and requested registered action-permission keys and persists the role definition and preset/custom metadata. Role assignment is a separate Control Plane transaction that:

  1. validates the principal, WorkspaceMember, WorkspaceRole, same-workspace invariant, and administrator permission;
  2. creates or activates the WorkspaceMemberRole record;
  3. commits the Control Plane transaction; and
  4. returns the updated membership.

Revocation deletes or disables the authoritative Control Plane record. During the next five-minute synchronization, the worker removes the corresponding workspace_role#member tuple. Deleting a WorkspaceMember causes the worker to remove all role-membership tuples derived from its WorkspaceMemberRole rows and its structural workspace#member tuple.

User-defined role deletion is coordinated because downstream services own direct-grant tuples that may target the role. The Control Plane first marks the role deleting and blocks new assignments and grants. Each downstream service removes its grants and acknowledges cleanup; the Control Plane then removes the role's assignments and permissions and tombstones the role. No synchronization worker deletes another owner's tuple.

The platform_admin and tenant_admin presets are fully protected: their preset keys, names, permissions, and lifecycle cannot be changed through role-management APIs. All custom-role creation, permission changes, and deletion require an authorized administrator. Other presets, including support_user, cannot be deleted, but their permission mappings migrate gradually with the corresponding product actions.

A successful Control Plane response means the authoritative record was committed; it does not mean OpenFGA was updated immediately. Normal changes are expected to reach OpenFGA during the next five-minute synchronization cycle.

OpenFGA Relationship Model

The following schema shows the required shape. The workflow_project and workflow types form the first proof-of-concept product module and demonstrate the downstream contract. Future microservices register only resource types they actually own, using the same workspace-parent, ownership, direct-grant, and inheritance pattern. The source model may be split into modules when the deployed OpenFGA toolchain supports modular models, but the deployed artifact and model ID remain a single reviewed unit per store.

model
schema 1.1

type principal

type workspace_role
relations
define workspace: [workspace]
define member: [principal]
define active_member: member and member from workspace

type workspace
relations
define member: [principal]

# Registered action permissions. Role names never become relations.
define can_manage_workspace: [workspace_role#active_member]
define can_use_workspace: [workspace_role#active_member]
define workflow_can_create: [workspace_role#active_member]
define workflow_can_read: [workspace_role#active_member]
define workflow_can_edit: [workspace_role#active_member]
define workflow_can_delete: [workspace_role#active_member]

type workflow_project
relations
define workspace: [workspace]
define workspace_member: member from workspace

define can_create_workflow: ([principal, workspace_role#active_member] or workflow_can_create from workspace) and workspace_member
define can_read: ([principal, workspace_role#active_member] or can_edit or workflow_can_read from workspace) and workspace_member
define can_edit: ([principal, workspace_role#active_member] or workflow_can_edit from workspace) and workspace_member
define can_delete: ([principal, workspace_role#active_member] or workflow_can_delete from workspace) and workspace_member

type workflow
relations
define project: [workflow_project]

define can_read: ([principal, workspace_role#active_member] or can_edit or can_read from project) and workspace_member from project
define can_edit: ([principal, workspace_role#active_member] or can_edit from project) and workspace_member from project
define can_delete: ([principal, workspace_role#active_member] or can_delete from project) and workspace_member from project

The specific inheritance above is illustrative until the platform and product permission matrix is approved. The structural rules are normative:

  • every top-level downstream object has exactly one workspace relation;
  • every child object has exactly one relation to its owning parent;
  • each store references exactly one workspace:<workspace-uid> root and no account object;
  • role names are dynamic Control Plane data; only registered action-permission keys appear as OpenFGA relations;
  • inherited Control Plane action permissions reach a resource through its workspace relation;
  • child permissions may inherit only from their declared parent;
  • every resource action is intersected with structural workspace membership, so deleting workspace#member makes stale direct and role grants ineffective;
  • direct grants are written to action relations such as can_read, can_edit, and can_delete and may target a principal or a same-workspace workspace_role#active_member userset; and
  • downstream object types define only structural parent relations and action permissions; they do not define intermediate resource-role relations.

Example tuples are:

# Control Plane-owned membership tuple
workspace:ws-01#member@principal:prn-01
workspace:ws-01#member@principal:prn-02

# Control Plane-owned role scope and WorkspaceMemberRole membership tuples
workspace_role:role-platform-admin#workspace@workspace:ws-01
workspace_role:role-platform-admin#member@principal:prn-01
workspace_role:role-data-engineer#workspace@workspace:ws-01
workspace_role:role-data-engineer#member@principal:prn-02
workspace_role:role-research-lead#workspace@workspace:ws-01

# Control Plane-owned role action permissions
workspace:ws-01#can_manage_workspace@workspace_role:role-platform-admin#active_member
workspace:ws-01#workflow_can_create@workspace_role:role-data-engineer#active_member
workspace:ws-01#workflow_can_edit@workspace_role:role-data-engineer#active_member

# Workflow service-owned hierarchy and direct grants
workflow_project:project-01#workspace@workspace:ws-01
workflow:workflow-01#project@workflow_project:project-01
workflow:workflow-01#can_edit@principal:prn-02
workflow:workflow-01#can_read@principal:prn-03
workflow:workflow-01#can_read@workspace_role:role-research-lead#active_member

OpenFGA usersets and parent-child traversal are the standard mechanisms for group-like roles and inherited access. See the official guidance for multi-tenant SaaS and microservices authorization.

Control Plane Membership Contract

Workspace role API

The Control Plane owns workspace-role creation and management. Authorized administrators use:

GET /v1/workspaces/{workspaceId}/roles?cursor={cursor}&limit={limit}
POST /v1/workspaces/{workspaceId}/roles
PATCH /v1/workspaces/{workspaceId}/roles/{roleUid}
DELETE /v1/workspaces/{workspaceId}/roles/{roleUid}

Each role response includes its immutable roleUid, mutable display name and description, kind (preset or custom), optional immutable presetKey, selected registered permissionKeys, lifecycle status, and version. The preset catalog is defined above; roleUid is workspace-specific. A custom role has no presetKey.

Role assignment APIs accept roleUid, never a display name. Role creation validates permission keys against the registered platform and product actions for the workspace. A role cannot be assigned outside its workspace. Only authorized administrators may create, edit, or delete custom roles. The two administrator presets are immutable, and no preset role may be deleted.

Downstream grant writers require a workspace-bound Control Plane role-resolution contract that returns only the role UID, owning workspace UID, lifecycle status, and grant eligibility. It is used to reject stale, deleting, or cross-workspace role targets; it does not expose role membership. The exact endpoint may be a single-role lookup or a cached view of the roles endpoint, but it must use the Control Plane API rather than its database.

Public membership API

The Control Plane exposes workspace memberships to authorized administrators:

GET /v1/workspaces/{workspaceId}/role-memberships?cursor={cursor}&limit={limit}

Each item contains:

{
"workspaceMemberUid": "wsm-01",
"workspaceId": "ws-01",
"principalId": "prn-02",
"principalKind": "person",
"roles": [
{
"roleUid": "role-data-analyst",
"workspaceMemberRoleUid": "wmr-01",
"name": "data_analyst",
"kind": "preset",
"presetKey": "data_analyst"
},
{
"roleUid": "role-data-engineer",
"workspaceMemberRoleUid": "wmr-02",
"name": "data_engineer",
"kind": "preset",
"presetKey": "data_engineer"
}
],
"status": "active",
"version": 42,
"updatedAt": "2026-08-07T10:00:00Z"
}

Create, update, and revoke operations use the same membership resource and return its new version. Responses contain canonical IDs and no OpenFGA tuple strings, store IDs, or credentials.

Internal workspace roles-and-memberships API

The workspace-bound Cluster Agent lists the role catalog and authoritative role assignments for its workspace from:

GET /internal/v1/workspaces/{workspaceId}/roles-and-memberships

The response contains:

  • workspace ID, which must match the workspace bound to the store;
  • every preset and user-defined WorkspaceRole available to WorkspaceMember records in that workspace, including its permission keys;
  • the active WorkspaceMember records and their WorkspaceMemberRole assignments by stable UID; and
  • the time at which the complete response was generated.

An example response is:

{
"workspaceId": "ws-01",
"generatedAt": "2026-08-07T10:00:00Z",
"roles": [
{
"roleUid": "role-platform-admin",
"name": "platform_admin",
"kind": "preset",
"presetKey": "platform_admin",
"permissionKeys": ["can_manage_workspace", "can_use_workspace", "workflow_can_create", "workflow_can_read", "workflow_can_edit", "workflow_can_delete"],
"status": "active"
},
{
"roleUid": "role-data-engineer",
"name": "data_engineer",
"kind": "preset",
"presetKey": "data_engineer",
"permissionKeys": ["can_use_workspace", "workflow_can_create", "workflow_can_read", "workflow_can_edit"],
"status": "active"
},
{
"roleUid": "role-research-lead",
"name": "Research Lead",
"kind": "custom",
"presetKey": null,
"permissionKeys": ["can_use_workspace", "workflow_can_read", "workflow_can_edit"],
"status": "active"
}
],
"memberships": [
{
"workspaceMemberUid": "wsm-01",
"principalId": "prn-02",
"principalKind": "person",
"roleAssignments": [
{
"workspaceMemberRoleUid": "wmr-02",
"workspaceRoleUid": "role-data-engineer"
}
],
"status": "active",
"version": 42
}
]
}

The endpoint returns one complete workspace snapshot. This is appropriate for the initial expected size of fewer than 100 roles and memberships per workspace. If that assumption changes materially, pagination or an incremental protocol can be designed later. The worker translates these domain records into OpenFGA tuples; the Control Plane API does not expose OpenFGA credentials or require the worker to understand the Control Plane database schema.

The permission arrays in this example demonstrate the contract shape only; they are not the approved permission matrix for those preset roles.

Only the Cluster Agent identity bound to the path workspace may call this internal endpoint.

Control Plane membership synchronization flow

The worker synchronizes workspace memberships, role definitions, user-role assignments, and the actions configured for each role. It performs a complete synchronization at startup and every five minutes. Creating, renaming, assigning, or deleting a user-defined role is a data change and does not publish an OpenFGA model. Only introducing, renaming, or changing the semantics of a registered action relation is an authorization-model change and follows the immutable model deployment process.

Downstream Service Integration Contract

Object identifiers and workspace binding

Every OpenFGA object uses <registered-type>:<immutable-service-id>, for example workflow:01J.... The service-owned ID must be opaque, must not contain personal data, and must never be reused after deletion. Callers pass typed IDs to the shared adapter; they do not supply an arbitrary OpenFGA object string.

An object ID is unique within its registered type and workspace store. The authoritative service record also stores workspace_id, even when the workspace can be reached through another parent. Every top-level object has exactly one workspace parent, every child has exactly one owning parent, and moving an object between workspaces is implemented as a controlled delete-and-recreate migration of its authorization graph.

The initial design does not allow a direct grant to bypass workspace membership. A grant target must be either an active principal with a WorkspaceMember record for the object's workspace or an active WorkspaceRole owned by that workspace. Cross-workspace sharing requires a separate product and threat-model decision before the model is expanded.

Design-time registration

A downstream service registers each access-controlled resource type through a reviewed authorization module. The registration includes:

  • globally unique OpenFGA type name, such as workflow_project;
  • owning service and operational contact;
  • authoritative database table or aggregate;
  • required workspace or parent relation;
  • writable action permissions, such as can_read, can_edit, and can_delete;
  • the registered workspace actions inherited by each resource action;
  • tuple creation and deletion lifecycle;
  • full-state synchronization query and orphan-removal policy;
  • model assertions for allow and deny cases; and
  • migration and rollback behavior.

Type names are never reused by another service. Renaming a type or relation is a model-and-data migration, not an in-place edit.

Runtime object lifecycle

When a service creates an access-controlled object, it must:

  1. authorize creation against the parent object or workspace;
  2. commit the domain object and an authorization outbox event in one database transaction;
  3. synchronize its mandatory parent tuple and initial grants;
  4. keep the object in authorization_pending state until synchronization succeeds; and
  5. expose the object only after authorization is ready.

For a child resource such as a workflow, the owning service writes the child-to-parent tuple. It does not copy workspace memberships or inherited role grants onto the child.

When a service deletes a resource, it first prevents new operations, removes direct grants and hierarchy tuples, then deletes or tombstones the domain record. Retries treat already-missing tuples as success. A periodic orphan scan removes stale tuples owned by that service.

Direct grants

Direct sharing is a domain mutation owned by the service. The service persists the grant record for display, audit, uniqueness, and repair, then synchronizes the equivalent tuple. Grant APIs accept a canonical principal ID; they do not accept email addresses or raw Keycloak subjects.

A direct grant writes a specific action relation and may target one principal, such as workflow:workflow-01#can_read@principal:prn-03, or a role userset, such as workflow:workflow-01#can_read@workspace_role:role-research-lead#active_member. The downstream service owns the action grant, while the Control Plane owns the role, its workspace, and its members. The service resolves and stores the stable roleUid; it does not persist the role display name as the grant target. The tuple-writer guard must verify through authoritative Control Plane data that the role belongs to the same workspace as the resource.

The granting principal must have an explicit permission such as can_share. A user cannot grant a permission stronger than the model permits, and a direct grant never changes Control Plane membership.

Authorization checks

Services use a shared adapter that requires:

principal_id
permission
object_type
object_id
workspace_id

The adapter constructs the OpenFGA subject and object, supplies the configured store and model IDs, sets a deadline, emits decision metrics, and returns one of allowed, denied, or unavailable. Callers must not collapse unavailable into denied internally, because operational failures need different alerts and HTTP responses.

For collection endpoints, services first restrict candidates by workspace and domain filters, then use ListObjects, batch checks, or a permission-aware index as appropriate. OpenFGA is not used to fetch or sort resource metadata.

Downstream tuple and authorization flow

This sequence combines the three operations every downstream microservice must support: synchronizing resource and grant tuples, listing visible objects, and checking a protected CRUD action. Workflow supplies the concrete POC tuple examples, but the participants and flow are service-neutral.

Model and Store Lifecycle

Authorization models are immutable. Every deployed change creates a new model ID, and production callers must explicitly send the configured model ID instead of relying on the latest model. This is the behavior recommended in OpenFGA's immutable model guidance.

The model pipeline must:

  1. combine the core module with installed product modules;
  2. lint and validate the model;
  3. run positive, negative, inheritance, tenant-isolation, and machine-principal assertions;
  4. identify tuple migrations required by the change;
  5. write the new immutable model to a test store;
  6. run integration and performance tests;
  7. write the model to the workspace store without activating it;
  8. migrate or backfill tuples where required;
  9. deploy callers configured with the new model ID; and
  10. retain the previous model ID for rollback while it remains data-compatible.

Additive changes should deploy the model before callers use the new relation. Destructive or renamed relations require a staged tuple migration. OpenFGA documents equivalent sequencing in its model migration guidance.

The workspace is not Active for authorization-dependent services until the store exists, the approved model is loaded, the first complete Control Plane synchronization succeeds, and a readiness check passes.

Synchronization and Repair

Delivery semantics

The Control Plane synchronization worker does not consume change events. On every run it constructs the complete desired Control Plane tuple set from the API response and applies only the differences. Repeated writes and missing deletes converge to success.

Downstream synchronization workers may use transactional outbox events for resource lifecycle changes. These events are delivered at least once, carry stable IDs and owner versions, and use idempotent tuple writes. Writes and deletes that must change together use one OpenFGA transaction and respect the server's configured tuple-per-write limit.

Synchronization workers retry transient failures with backoff. A Control Plane snapshot validation error fails that synchronization cycle, preserves the existing tuples, and raises an alert. A non-retryable downstream outbox event moves to a dead-letter state and raises an alert containing the event ID, owner, relation, and model ID but no personal data.

Control Plane full-snapshot synchronization

At startup and every five minutes, the Cluster Agent calls the Control Plane roles-and-memberships API and compares the complete desired state with only the Control Plane-owned tuples in the workspace store. Its owned set includes workspace membership, role scope, role membership, and role action-permission tuples. It writes missing desired tuples and deletes stale tuples in that ownership set. It never queries the Control Plane database and never changes a downstream resource or direct-grant tuple.

Assigning the same principal in another workspace is an independent WorkspaceMember operation and is returned only by that workspace's snapshot endpoint.

Downstream synchronization and repair

Each downstream service synchronizes resource changes through its own worker. It also periodically derives its complete desired tuple set from its database to repair missing tuples and remove owned orphans. Large service-owned resource sets may require pagination and rate limiting.

Consistency

The default authorization path uses normal cached checks when enabled. After a grant or revocation has been written and an immediate confirmation check is required, the adapter requests higher consistency. Higher consistency bypasses the query cache, but it cannot make a change visible before the applicable synchronization worker has written it. See OpenFGA's consistency modes.

The platform defines and monitors two separate service-level objectives:

  • synchronization delay: time from authoritative database commit to successful tuple write; and
  • check latency and availability: time and success rate for OpenFGA decisions.

The exact thresholds must be set from product security requirements and measured load tests. They must be configuration, not hard-coded behavior.

Failure and Security Behavior

ConditionRequired behavior
Missing or invalid tokenReturn 401; do not call OpenFGA.
Token cannot be mapped to an active principalReturn 401 or 403 according to the API authentication contract; never fall back to email or role claims.
Resource does not exist in the service databaseReturn 404 before revealing authorization state.
OpenFGA returns allowed: falseReturn 403 and record a deny decision without sensitive tuple details.
OpenFGA times out or is unavailableFail closed and return 503; do not convert a dependency failure into an allow or cache an allow indefinitely.
Required store or model ID is absent or mismatchedFail service readiness.
Initial resource tuples are pendingKeep the resource unavailable and report provisioning state; do not bypass authorization for the creator.
Control Plane synchronization failsPreserve existing tuples, mark the worker unhealthy, retry, and alert when the last successful synchronization exceeds the configured maximum age.
Last successful Control Plane synchronization is too oldWorkspace ingress or the authorization adapter fails closed for operations that depend on Control Plane memberships.
Membership revocation is requestedDisable the authoritative membership immediately; the tuple is removed during the next five-minute synchronization. Use emergency Keycloak disablement when that delay is unacceptable.
Synchronization worker derives an invalid tuple for the active modelFail that synchronization item or cycle, preserve the previously valid state, and alert; do not skip it silently.
A synchronization worker finds another owner's tupleReport it as cross-owner drift; do not mutate it.

Because the workspace pulls a complete Control Plane snapshot every five minutes, normal revocation is eventually consistent. Its expected stale-access window is up to five minutes plus API and tuple-write processing time. If synchronization remains unhealthy beyond the configured maximum age, authorization paths that depend on workspace membership fail closed. High-risk emergency revocation also disables the Keycloak identity or client to prevent new tokens, while existing token lifetime remains a separate bound.

Authorization decision logs include request correlation ID, principal ID, workspace ID, object type and ID, permission, decision, model ID, consistency mode, latency, and caller. They exclude access tokens and personal profile fields.

Provisioning and Deprovisioning

Account provisioning

  1. The Control Plane creates the account and initial account membership.
  2. Keycloak realm and clients are created.
  3. The initial principal is bound to its Keycloak identity.
  4. No account object or account tuple is written to OpenFGA.
  5. No workspace role is assigned until the principal receives a WorkspaceMember record.

Workspace provisioning

  1. The Control Plane creates the workspace, records its account association internally, and seeds the eight preset WorkspaceRole records with their approved action permissions.
  2. The Control Plane creates the initial administrator's WorkspaceMember and WorkspaceMemberRole assignments using preset role UIDs.
  3. Infrastructure and the Cluster Agent are provisioned.
  4. OpenFGA and its persistent datastore are deployed privately in the workspace.
  5. The provisioner creates the workspace store and writes the approved model.
  6. The Cluster Agent calls the Control Plane roles-and-memberships API and applies the complete response.
  7. Installed downstream services contribute their model modules and deploy their authorization synchronization workers.
  8. Store ID, pinned model ID, and last successful Control Plane synchronization time are recorded in workspace status.
  9. Authorization readiness tests run before the workspace becomes Active.

Workspace deletion

  1. Block new user operations and role assignments for the workspace.
  2. Stop downstream services and their authorization synchronization workers.
  3. Apply or archive required authorization audit records.
  4. Delete the workspace OpenFGA store and credentials as part of tenant teardown.
  5. Mark outstanding downstream synchronization events terminal so they cannot recreate tuples.
  6. Destroy the OpenFGA datastore with the rest of the workspace infrastructure.

Gaps and Required Work

SystemCurrent gapRequired work
Control PlanePrincipal.workspaceMembers is not populated, fixed roles have no shared action-permission contract, and required role and membership APIs do not exist.Populate workspace members; seed the eight preset WorkspaceRole records; add stable preset metadata and permissionKeys; enforce immutable administrator presets and administrator-only custom-role management; implement membership CRUD, role resolution, and the complete internal roles-and-memberships snapshot API.
Control Plane synchronizationNo five-minute full-snapshot worker, tuple comparison, readiness rule, or synchronization health is implemented.Fetch the complete snapshot at startup and every five minutes; derive and compare only Control Plane-owned tuples; apply bounded idempotent changes; preserve valid tuples on snapshot failure; record last success and errors; fail readiness when synchronization is too stale.
OpenFGA platformStore lifecycle, model deployment, pinned model IDs, scoped tuple writes, production configuration, and common telemetry are not standardized.Automate one store per workspace; add model validation and immutable deployment; distribute pinned model IDs; provide the shared check adapter and guarded write path; configure private networking, credentials, backup, availability, metrics, and recovery procedures.
Downstream servicesResource registration, parent tuples, direct action grants, checks, synchronization, and repair are inconsistent or absent.Register owned resource types and action relations; persist workspace_id, parents, and direct grants; validate role UIDs and workspace scope; add transactional synchronization, periodic repair, object readiness, action checks, collection filtering, and decision telemetry.
UI and BFFThe frontend currently maps preset roles to behavior and does not have a common custom-role administration or action-query contract.Keep existing logic only for unmigrated actions; migrate each action to a backend can_* query backed by OpenFGA; add administrator-only custom-role management; use canonical IDs for sharing; handle backend 403 and 503 responses.

Delivery Plan

Phase 1: Decisions and foundation

  • Approve the store-per-workspace topology, canonical identifier contract, custom-role contract, and initial action-permission namespace required by the Workflow POC.
  • Implement the core OpenFGA model and assertion suite.
  • Establish model ownership, tuple ownership, and deployment credentials.
  • Define the common downstream registration manifest and adapter interfaces.

Phase 2: Control Plane synchronization

  • Implement WorkspaceMember, WorkspaceRole, and WorkspaceMemberRole APIs, Principal.workspaceMembers, preset seeding, and custom-role action-permission configuration.
  • Enable user-defined role CRUD with permission validation, audit, same-workspace enforcement, and coordinated deletion.
  • Implement the complete roles-and-memberships API and synchronization-health reporting.
  • Add the Cluster Agent five-minute full-snapshot synchronization worker.
  • Gate workspace readiness on store, model, and membership state.

Phase 3: Workflow proof of concept

  • Onboard workflow_project and workflow as the first service-owned resource types.
  • Backfill workspace, project, workflow, and existing action-grant relationships.
  • Migrate Workflow actions one at a time, run shadow checks alongside the current frontend preset-role logic, and compare decisions.
  • Exercise create, read, update, delete, share, revoke, agent outage, OpenFGA outage, and model rollback.

Phase 4: Product rollout

  • Onboard additional downstream microservices one at a time after each service registers the resource types it actually owns.
  • For each service: deploy the additive model, backfill tuples, repair differences, shadow, enforce, and then remove the old backend authorization path.
  • Remove each legacy frontend role mapping only after its corresponding backend action check has migrated and reached decision parity.

Acceptance Criteria

The architecture is ready for general rollout when:

  • every person and machine request resolves to one canonical Control Plane principal;
  • Principal.workspaceMembers, workspace-role APIs, and membership APIs return authoritative data using stable UIDs;
  • every workspace contains the eight preset roles and can create a user-defined role without an OpenFGA model deployment;
  • WorkspaceMemberRole assignment, permission change, and revocation converge to the expected OpenFGA tuples during the next five-minute synchronization;
  • each workspace has a private store and pinned tested model ID;
  • a new or changed membership is synchronized within five minutes plus API and tuple-write processing time;
  • revocation behavior and its maximum stale window are measured and documented;
  • every tuple category has exactly one accountable owning system;
  • every downstream top-level resource has one workspace parent and every child has one service-owned parent edge;
  • services deny by default and distinguish authorization denial from OpenFGA unavailability;
  • workflow shadow checks show no unexplained allow decisions and all expected allows are understood;
  • migrated frontend paths ask the backend for action decisions and no longer maintain their own preset-role mapping;
  • a workspace store can be rebuilt solely from Control Plane and downstream authoritative databases;
  • model rollback, datastore restore, and stalled synchronization-worker procedures have passed an operational exercise; and
  • the initial Workflow action mappings have passed authorization review.

Open Questions Requiring Approval

The design intentionally makes the architecture concrete while leaving these product and operational values for explicit approval:

  • custom-role limits and naming rules;
  • the maximum failed-synchronization age before authorization fails closed;
  • the OpenFGA availability and recovery objectives;
  • the first services and resource operations after the workflow proof of concept; and
  • audit-event retention and access-review frequency.

These decisions change configuration, model relations, or SLO values; they do not change the ownership and synchronization architecture defined above.