Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.emergence.ai/llms.txt

Use this file to discover all available pages before exploring further.

Multi-Tenancy

CRAFT implements multi-tenancy at every layer of the stack. Each organization is fully isolated through a combination of identity separation (Keycloak realms), authorization scoping (OpenFGA relationship tuples), and data partitioning (org_id filtering on every query). This page describes the architecture that makes this work.

Isolation Layers

The platform enforces tenant isolation through four complementary mechanisms:

Identity Isolation

Each organization is a separate Keycloak realm. Users, groups, sessions, and identity provider configurations are completely isolated between realms. A user in acme-corp cannot see or interact with users in globex.

Authorization Isolation

OpenFGA relationship tuples are scoped to specific resources. Permission checks always include the resource ID, ensuring a user in one organization cannot access resources in another even if they somehow obtain a valid token.

Data Isolation

Every database query includes an organization_id filter derived from the JWT token. This is enforced at the application layer — queries without org_id filters are treated as security vulnerabilities.

Service Isolation

Each platform service (Governance, Assets, Utils) owns its own PostgreSQL database. There are no cross-service foreign keys, preventing accidental data leakage through join queries.

How org_id Flows Through the System

The organization ID is the foundation of tenant isolation. It flows through every request:
1

Authentication

The user authenticates against their organization’s Keycloak realm. The JWT token’s iss (issuer) claim contains the realm name, which becomes the org_id.
iss: "https://keycloak.example.com/realms/acme-corp"
                                           ^^^^^^^^^ org_id
2

Token Validation

The Governance service validates the JWT using the realm’s JWKS public keys. The org_id is extracted from the verified issuer claim and stored in the Auth object. Users in the master realm (platform developers) have org_id = null.
3

Request Processing

Every API endpoint receives the Auth object via dependency injection. The org_id is used to:
  • Filter list queries to return only the organization’s resources
  • Scope write operations to associate new resources with the correct organization
  • Validate access via OpenFGA permission checks
4

Cross-Service Calls

When Assets or Utils need to check permissions, they forward the user’s JWT token to the Governance service via auto-generated SDK calls. The Governance service re-validates the token and checks OpenFGA — the org_id is never passed as a parameter that could be spoofed.

Dual Authorization Pattern

The platform uses a dual authorization pattern that combines database-level filtering with authorization checks:
List endpoints apply both database filtering and permission checks:
@router.get("/data")
async def list_data_connections(
    ctx: RequestContextDep,
    # Permission check: user can read resources in this project
    _: None = Depends(require_permission(
        Permissions.PROJECT,
        Permissions.PROJECT.actions.CAN_READ,
        HEADER_PROJECT_ID
    ))
):
    # Database filter: always scope by org_id from JWT
    connections = await service.list_data_connections(
        organization_id=ctx.org_id,    # From JWT, never from request body
        project_id=ctx.project_id,      # From X-Project-ID header
    )
This defense-in-depth approach means that even if a permission check has a bug, the database query will not return another organization’s data.
Security rules enforced across all services:
  • Never trust user-supplied org_id from request body or parameters. Always use auth.org_id from the JWT.
  • Never list resources without an org_id filter. Omitting the filter returns all organizations’ data.
  • Always use auth.org_id when creating new resources. This ensures the resource is correctly scoped to the authenticated user’s organization.

Database Architecture

Each service owns an independent PostgreSQL database with no cross-service foreign keys:
PostgreSQL Server
  keycloak_db          -- Keycloak identity data
  openfga_db           -- OpenFGA authorization tuples
  infisical_db         -- Infisical secrets data (present by default; unused if using ESO backend)
  governance_db        -- Organizations, projects
  assets_db            -- Artifacts, data connections, files, models
  utils_db             -- Data catalog, scheduling, context packs, memories
Every resource table includes an organization_id column:
ColumnTypePurpose
organization_idVARCHARTenant partition key, always populated from JWT
project_idVARCHARProject scope within the organization
created_atTIMESTAMPTZRecord creation timestamp (via TimestampMixin)
updated_atTIMESTAMPTZLast modification timestamp (via TimestampMixin)
Each service runs its own Alembic migrations independently. There are no cross-service migration dependencies, which means services can be upgraded independently without coordinating database schema changes.

Keycloak Realm Structure

When an organization is created, the Governance service provisions a complete Keycloak realm:
acme-corp (realm)
  Groups:
    org-owners       -- Organization owners
    org-admins       -- Organization administrators
    org-members      -- Standard members
    project-owners   -- Project-level owners
    project-admins   -- Project-level administrators
    project-developers  -- Project-level developers
    project-operators   -- Project-level operators
    project-viewers     -- Project-level viewers

  Mappers:
    groups-mapper    -- Ensures group memberships appear in JWT "groups" claim

  Clients:
    em-runtime-ui    -- Frontend application client (PKCE)

  Identity Providers:
    (configurable per realm: SAML, OIDC, LDAP, social login)

Service Account Cross-Tenant Access

Service accounts (background workers, automated processes) authenticate via the master realm and specify the target organization via headers:
HeaderPurpose
X-Org-IdTarget organization for the operation
X-On-Behalf-OfOriginal user ID for audit trails
These headers are only trusted when the caller passes all three service account checks:
  1. Token issued by master realm
  2. Client ID starts with svc-
  3. serviceAccount role in realm_access.roles
Regular user tokens cannot use these headers — the platform silently ignores them for non-service-account callers.

Service Startup and Dependencies

The multi-tenant infrastructure requires a specific startup order:
PostgreSQL -> Redis -> Keycloak -> OpenFGA -> [Secrets backend] -> Governance -> Assets/Utils
The Governance service must be running before Assets or Utils can validate permissions. If Governance is down, downstream services return 403 for all requests because permission checks fail.
make docker-run handles the startup order automatically via Docker Compose health checks. For manual local development, start make run-deps first, wait for readiness, then start Governance before Assets or Utils.

Next Steps

Organizations

Learn how organizations are provisioned with Keycloak realms.

Authentication

Understand the multi-realm JWT authentication flow.

Authorization

Explore OpenFGA permission inheritance across tenants.

Projects

See how projects provide the second level of scoping within tenants.