How Twenty CRM Implements Multi-Tenancy for Workspaces: Schema-Per-Tenant Architecture

Twenty CRM achieves multi-tenancy for workspaces by isolating each tenant in a dedicated PostgreSQL schema, using a centralized ORM manager to route all queries to the correct tenant context while maintaining global workspace metadata in a shared core schema.

Twenty CRM serves multiple organizations—referred to as workspaces—from a single application instance. To guarantee strict data isolation without the operational overhead of separate database instances, the platform implements a true multi-tenant architecture where every workspace receives its own isolated PostgreSQL schema. This design ensures that tenant data never commingles while allowing the application to scale efficiently on shared infrastructure.

How Twenty CRM Isolates Tenant Data with PostgreSQL Schemas

Instead of co-mingling all customer data in shared tables or maintaining separate database connections per tenant, Twenty creates a distinct PostgreSQL schema for each workspace. This approach provides strong data isolation at the database level while keeping the connection pool and application code unified.

Workspace Schema Naming Convention

The system generates a deterministic schema name for every workspace using the getWorkspaceSchemaName utility. Located in packages/twenty-server/src/engine/workspace-datasource/utils/get-workspace-schema-name.util.ts, this function converts the workspace UUID into a base36-encoded string prefixed with workspace_.

import { getWorkspaceSchemaName } from '@/workspace/utils/get-workspace-schema-name.util';

const schema = getWorkspaceSchemaName('8f6d5a7c-a3b2-4c9e-b1f0-d2e3f4a5b6c7');
// → "workspace_1wgvd1injqtife6y4rvfbu3h5"

This deterministic naming allows the system to resolve the correct schema for any workspace ID without additional database lookups.

The Global Workspace Registry

While tenant data lives in isolated schemas, global workspace metadata resides in the shared core schema. The WorkspaceEntity defined in packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts stores activation status, subdomain configuration, and display names in the core.workspace table. This registry enables the application to validate workspace existence and manage cross-cutting concerns before routing to tenant-specific schemas.

Core Components Managing Multi-Tenancy for Workspaces

Twenty's multi-tenancy implementation relies on three interconnected services that handle context propagation, data access, and performance optimization across tenant boundaries.

GlobalWorkspaceOrmManager: The Tenant Gateway

The GlobalWorkspaceOrmManager class in packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts serves as the primary abstraction for tenant-aware data operations. It provides two critical methods for interacting with workspace data:

  • getRepository(workspaceId, entity): Returns a TypeORM repository automatically bound to the workspace's specific schema.
  • executeInWorkspaceContext(callback, authContext): Executes arbitrary logic within a fully loaded workspace context, including metadata and permissions.

This manager ensures that all database queries target the correct schema without requiring developers to manually manage schema routing.

WorkspaceAuthContext: Injecting Tenant Identity

To maintain security boundaries, the system stores the current user's workspace affiliation in WorkspaceAuthContext. Defined in packages/twenty-server/src/engine/core-modules/auth/types/workspace-auth-context.type.ts, this object propagates the workspace ID through the request lifecycle. The ORM manager consumes this context to resolve the appropriate schema, ensuring that data access is automatically scoped to the authenticated tenant.

WorkspaceCacheService: Optimizing Per-Tenant Performance

Loading workspace metadata for every request would create significant database overhead. The WorkspaceCacheService in packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts caches object maps, field definitions, and permission sets per workspace. When GlobalWorkspaceOrmManager initializes a workspace context, it retrieves cached metadata rather than querying the core schema repeatedly, maintaining low latency for multi-tenant operations.

Provisioning New Workspace Tenants

Creating a new workspace involves both metadata registration and physical schema initialization. The process follows a strict sequence to ensure tenant isolation from inception.

When a user creates a workspace, the system first inserts a record into the core.workspace table via WorkspaceEntity. It then invokes getWorkspaceSchemaName to compute the tenant's schema identifier. Finally, the workspace export service located at packages/twenty-server/src/database/commands/workspace-export/workspace-export.service.ts executes CREATE SCHEMA IF NOT EXISTS followed by metadata-driven migrations that instantiate the required tables within the new tenant schema.

Working with Workspace Data: Practical Code Examples

The following patterns demonstrate how application code interacts with the multi-tenant architecture using Twenty's ORM abstractions.

Computing Schema Names for Raw SQL

When executing low-level SQL operations outside TypeORM repositories, use the schema utility to resolve the correct identifier:

import { getWorkspaceSchemaName } from '@/workspace/utils/get-workspace-schema-name.util';

const schema = getWorkspaceSchemaName('8f6d5a7c-a3b2-4c9e-b1f0-d2e3f4a5b6c7');
// → "workspace_1wgvd1injqtife6y4rvfbu3h5"

Accessing Repositories by Workspace ID

Services inject GlobalWorkspaceOrmManager to obtain repositories scoped to specific tenants:

@Injectable()
export class PersonService {
  constructor(
    private readonly globalWorkspaceOrm: GlobalWorkspaceOrmManager,
  ) {}

  async findAll(workspaceId: string): Promise<Person[]> {
    const repo = await this.globalWorkspaceOrm.getRepository(
      workspaceId,
      'person',
    );
    return repo.find();
  }
}

Executing Logic Within a Workspace Context

For operations requiring full workspace metadata and permissions, wrap execution inside the context manager:

await this.globalWorkspaceOrm.executeInWorkspaceContext(
  async () => {
    // All repository calls here automatically target the workspace schema
    // Auth context contains workspace ID and role information
    const notes = await noteRepo.find({ where: { title: Like('%meeting%') } });
    return notes;
  },
  { workspace: { id: workspaceId, user: { id: systemUserId } } },
);

Summary

Twenty CRM implements multi-tenancy for workspaces through a robust schema-per-tenant architecture that balances isolation with performance. Key implementation details include:

  • Each workspace receives a dedicated PostgreSQL schema named via getWorkspaceSchemaName to ensure deterministic routing.
  • Global workspace metadata resides in the shared core.workspace table managed by WorkspaceEntity.
  • GlobalWorkspaceOrmManager serves as the central gateway for schema-bound repositories and tenant context execution.
  • WorkspaceAuthContext propagates tenant identity from the authentication layer to the data layer.
  • WorkspaceCacheService eliminates redundant metadata queries by caching workspace configurations per tenant.

Frequently Asked Questions

How does Twenty CRM ensure data isolation between workspaces?

Twenty enforces isolation at the database level by storing each workspace's data in a separate PostgreSQL schema. The GlobalWorkspaceOrmManager guarantees that every query executes against the correct schema by resolving the workspace ID from WorkspaceAuthContext. This schema-per-tenant approach prevents cross-contamination of data between organizations while allowing shared application infrastructure.

What performance optimizations exist for multi-tenant queries?

The platform uses WorkspaceCacheService to cache metadata, permission maps, and feature flags for each workspace in memory. When application code enters a workspace context via executeInWorkspaceContext, the system retrieves cached configuration rather than querying the core schema repeatedly. This caching layer minimizes database round trips and maintains consistent latency regardless of the number of active workspaces.

Can Twenty CRM support single-tenant deployment without schema separation?

While the codebase is architected for schema-based multi-tenancy, the underlying TypeORM configuration could theoretically target a single schema. However, the getWorkspaceSchemaName utility and GlobalWorkspaceOrmManager are designed specifically for schema routing. Removing multi-tenancy would require significant modifications to the workspace provisioning logic in workspace-export.service.ts and the ORM manager's schema resolution mechanisms.

How are background jobs handled across multiple workspace tenants?

Background workers utilize the same GlobalWorkspaceOrmManager abstraction as HTTP requests. When processing jobs, the worker explicitly passes the target workspace ID to executeInWorkspaceContext or getRepository, ensuring the job operates within the correct tenant schema. The workspace cache allows workers to rapidly switch contexts between jobs targeting different organizations without reinitializing database connections.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →