How GraphQL APIs Are Implemented in Twenty CRM: Dynamic Schema Generation Explained

Twenty CRM generates GraphQL APIs dynamically per workspace using NestJS and the Yoga GraphQL driver, where schemas and resolvers are built at runtime from workspace metadata rather than defined statically.

The open-source Twenty CRM platform implements a metadata-driven GraphQL architecture that adapts to each workspace's custom data model. Unlike traditional GraphQL implementations that rely on static schema definitions, Twenty constructs per-workspace schemas on-demand by reading object and field metadata from the database. This approach enables multi-tenant isolation while supporting user-defined custom objects without redeployment.

Runtime Schema Generation Architecture

The core innovation in Twenty's GraphQL implementation is its dynamic schema factory pattern. When a request hits the /graphql endpoint, the system does not serve a pre-built schema; instead, it generates a tailored schema specific to that workspace's current configuration.

The bootstrap process begins in packages/twenty-server/src/app.module.ts, where the AppModule imports GraphQLModule.forRootAsync() using a custom GraphQLConfigService. This configuration runs on every request to determine which schema to serve.

In packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts, the createGqlOptions() method builds a Yoga driver configuration. The critical piece is the conditionalSchema callback, which:

  1. Loads the workspace context
  2. Authenticates the user
  3. Calls WorkspaceSchemaFactory.createGraphQLSchema() to generate the schema

This ensures that no two workspaces share the same GraphQL schema definition, providing strict isolation between tenants.

Metadata Extraction and Type Generation

Before generating GraphQL types, the system extracts flat metadata from the workspace's datasource. The WorkspaceSchemaFactory class in packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts orchestrates this process.

The factory retrieves:

  • Object metadata: Custom and standard CRM objects (e.g., contacts, companies)
  • Field metadata: Data types, relationships, and validation rules
  • Index metadata: Database indexes for query optimization
  • Application metadata: UI configuration and visibility settings

Once extracted, the WorkspaceGraphQLSchemaGenerator (located in the workspace-schema-builder directory) transforms this flat metadata into GraphQL type definitions (typeDefs). The ScalarsExplorerService discovers and registers custom scalar types used by the generated schema, ensuring proper serialization for dates, JSON fields, and other custom types.

This metadata-driven approach means that when an administrator adds a new custom field to an object, that field automatically appears in the GraphQL schema without requiring a server restart or code deployment.

Resolver Generation Factory Pattern

Twenty implements resolvers using a factory pattern that maps GraphQL operations to internal service methods. The WorkspaceResolverFactory in packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts iterates over every object metadata definition and generates a resolver map for each.

For every object, the factory creates standard CRUD operations through specialized factory classes:

  • CreateOneResolverFactory: Implements createOne mutations
  • FindManyResolverFactory: Implements list queries with pagination
  • FindOneResolverFactory: Implements single-record queries
  • UpdateOneResolverFactory: Implements partial updates
  • DeleteOneResolverFactory: Implements deletions
  • AggregateResolverFactory: Implements count and aggregation queries

Each factory class lives in packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/ and encapsulates the logic for mapping GraphQL arguments to Twenty's internal service layer. The generated resolver map is then combined with the typeDefs via makeExecutableSchema from graphql-tools to produce the final executable GraphQLSchema.

Conditional Resolvers and Metadata-Driven Features

Not all objects support the same operations. Twenty uses conditional resolver generation to expose features only when the underlying metadata supports them.

The WorkspaceResolverBuilderService in packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver-builder.service.ts evaluates duplicateCriteria definitions in the object metadata. If an object defines criteria for detecting duplicates, the builder automatically adds:

  • findDuplicates: Query resolver for identifying potential duplicate records
  • mergeMany: Mutation resolver for consolidating duplicate records

If the metadata lacks duplicateCriteria, these fields do not exist in the generated schema. This prevents client applications from attempting unsupported operations and keeps the schema documentation accurate to actual capabilities.

Querying the Dynamic GraphQL API

Twenty exposes standard CRUD operations following a predictable naming convention: <objectName><Operation>. For example, operations on a Contact object generate contactsFindMany, contactsCreateOne, and contactsUpdateOne.

Querying Multiple Records

query GetContacts($workspaceId: ID!) {
  contactsFindMany(workspaceId: $workspaceId, limit: 10) {
    edges {
      node {
        id
        firstName
        lastName
        email
      }
    }
  }
}

The contactsFindMany resolver is generated at runtime by FindManyResolverFactory, which handles pagination, filtering, and sorting based on the workspace's field definitions.

Creating Records

mutation CreateContact($workspaceId: ID!, $data: ContactCreateOneInput!) {
  contactsCreateOne(workspaceId: $workspaceId, data: $data) {
    id
    firstName
    lastName
    createdAt
  }
}

Mutation inputs like ContactCreateOneInput are generated dynamically based on the writable fields defined in the metadata for that specific workspace.

Duplicate Detection (Conditional)

query FindDuplicateContacts($workspaceId: ID!, $filter: ContactFilter!) {
  contactsFindDuplicates(workspaceId: $workspaceId, filter: $filter) {
    id
    firstName
    lastName
    duplicateScore
  }
}

This query is only available if the Contact object metadata includes duplicateCriteria. If not defined, the field will not appear in the schema introspection results.

Using the Generated TypeScript SDK

Twenty's frontend uses auto-generated TypeScript types from the GraphQL schema. Located at packages/twenty-front/src/generated/graphql.ts, these types ensure end-to-end type safety between the dynamic API and the React frontend.

import { GraphQLClient } from 'graphql-request';
import { ContactsFindManyQuery } from './generated/graphql';

const client = new GraphQLClient('/graphql', {
  headers: { Authorization: 'Bearer <jwt>' }
});

async function loadContacts(workspaceId: string) {
  const data = await client.request<ContactsFindManyQuery>(
    `
    query($workspaceId: ID!) {
      contactsFindMany(workspaceId: $workspaceId) {
        edges { node { id firstName lastName email } }
      }
    }
  `,
    { workspaceId }
  );

  return data.contactsFindMany.edges.map((e) => e.node);
}

Caching and Performance Optimization

To prevent schema generation from becoming a bottleneck, Twenty implements aggressive caching via WorkspaceCacheStorageService. Generated schemas and resolver maps are cached and only rebuilt when metadata changes trigger an invalidation event. This allows the system to serve millions of requests through the same generated schema without regenerating it on every HTTP call, while still maintaining the flexibility to regenerate immediately when configuration changes.

Summary

  • Dynamic Schema Generation: Twenty creates per-workspace GraphQL schemas at runtime using WorkspaceSchemaFactory, enabling custom objects without code changes.
  • Factory Pattern: Resolvers are built by WorkspaceResolverFactory using specialized factory classes for each CRUD operation, located in the workspace-resolver-builder directory.
  • Metadata-Driven: Schemas reflect the current state of workspace metadata, with conditional resolvers appearing only when features like duplicate detection are configured.
  • Security Isolation: The GraphQLConfigService ensures each request receives a schema specific to that workspace, preventing data leakage between tenants.
  • Performance: Schema generation is cached via WorkspaceCacheStorageService, making the dynamic approach production-ready for high-traffic CRM workloads.

Frequently Asked Questions

What GraphQL server does Twenty CRM use?

Twenty uses GraphQL Yoga as the GraphQL server driver, configured through NestJS's @nestjs/graphql module. The specific configuration happens in graphql-config.service.ts, where Yoga's conditionalSchema feature enables the dynamic per-workspace schema generation that Twenty relies on for multi-tenancy.

How does Twenty handle custom fields in GraphQL?

Custom fields automatically appear in the GraphQL schema because Twenty generates types from workspace metadata at runtime. When an admin adds a custom field through the UI, the metadata service updates the workspace definition, invalidates the cached schema, and the next request generates a new schema including that field. No server restart or code deployment is required.

Why doesn't Twenty use a static GraphQL schema?

A static schema would prevent true multi-tenancy with custom objects. Since each Twenty workspace can define its own unique objects and fields, a one-size-fits-all schema would either expose all possible fields (creating a massive, confusing API) or require redeployment for every configuration change. The dynamic approach in workspace-schema.factory.ts ensures each workspace sees only its relevant types and fields.

Where are the GraphQL types defined for the frontend?

Frontend TypeScript definitions live in packages/twenty-front/src/generated/graphql.ts. These types are generated using GraphQL Code Generator based on introspection of a workspace's schema. Because the schema is dynamic, Twenty ensures the frontend generates types against a workspace that includes all standard objects plus any custom configurations needed for type safety.

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 →