Skip to main content

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

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