Learn from OSS
Twenty

Architecture Overview

Twenty's system design — metadata engine, GraphQL/REST APIs, multi-tenant database, and real-time events

Architecture Overview

For Product Managers

This page explains how Twenty is structured as a software system. Focus on the diagrams and "Why It Matters" callouts to understand how the technical architecture supports features like custom objects, email sync, and workflow automation.

System Architecture Diagram

Twenty follows a modular monolith pattern — a single NestJS server handles API requests, while separate worker processes handle background jobs.

┌──────────────────────────────────────────────────────────────────┐
│                        USERS (Browser)                           │
└───────────────────────────────┬──────────────────────────────────┘

                   ┌────────────▼────────────┐
                   │   Twenty Server (NestJS) │
                   │                          │
                   │  ┌────────────────────┐  │
                   │  │  GraphQL API        │  │  ← /graphql (workspace data)
                   │  │  (GraphQL Yoga)     │  │  ← /metadata (schema config)
                   │  ├────────────────────┤  │
                   │  │  REST API           │  │  ← /rest/* (CRUD operations)
                   │  ├────────────────────┤  │
                   │  │  SSE Events         │  │  ← real-time DB change stream
                   │  ├────────────────────┤  │
                   │  │  Auth               │  │  ← JWT, OAuth, SSO, SAML
                   │  │  (Passport.js)      │  │
                   │  └────────────────────┘  │
                   └──────┬──────┬──────┬─────┘
                          │      │      │
           ┌──────────────┼──────┼──────┼──────────────┐
           │              │      │      │              │
    ┌──────▼──────┐ ┌─────▼────┐ │ ┌────▼───────┐ ┌───▼────────┐
    │ PostgreSQL   │ │  Redis   │ │ │ ClickHouse │ │  External  │
    │  (TypeORM)   │ │          │ │ │ (Analytics)│ │  Services  │
    │              │ │ • Cache  │ │ │            │ │            │
    │ • Core schema│ │ • Session│ │ └────────────┘ │ • Gmail    │
    │ • Workspace  │ │ • PubSub │ │                │ • Microsoft│
    │   schemas    │ └─────┬────┘ │                │ • Stripe   │
    │ • Metadata   │       │      │                │ • S3       │
    └──────────────┘  ┌────▼────┐ │                │ • OpenAI   │
                      │ BullMQ  │ │                └────────────┘
                      │ Workers │◄┘
                      │         │
                      │ • Email sync    │
                      │ • Calendar sync │
                      │ • Webhooks      │
                      │ • Workflows     │
                      │ • AI jobs       │
                      └─────────────────┘

Why It Matters

The server handles synchronous requests (API calls) while workers handle asynchronous jobs (email sync, webhooks, workflow execution). This separation means the API stays fast — email sync for 10,000 messages doesn't slow down loading a contact page. Redis acts as the bridge: BullMQ uses it as a job queue, and SSE subscriptions use it for pub/sub.

The Metadata Engine (Core Innovation)

The metadata engine is Twenty's most distinctive architectural feature. It enables custom objects without code changes.

How It Works

┌────────────────────────┐
│   Object Metadata DB   │  ← stores object definitions
│   (core schema)        │     (name, fields, relations, etc.)
└───────────┬────────────┘
            │ read at startup

┌────────────────────────┐
│  WorkspaceSchemaFactory │  ← generates GraphQL schema
│  (NestJS service)       │     from metadata definitions
└───────────┬────────────┘
            │ produces

┌────────────────────────┐
│  Dynamic GraphQL Schema │  ← per-workspace, includes
│  + REST endpoints       │     standard + custom objects
└───────────┬────────────┘
            │ serves

┌────────────────────────┐
│   Workspace Database   │  ← actual data tables
│   Tables (per tenant)  │     auto-created from metadata
└────────────────────────┘

What the Metadata Contains

Metadata EntityWhat It Stores
ObjectMetadataObject name, label, description, icon, permissions
FieldMetadataField name, type (text, number, currency, relation, etc.), validation
RelationMetadataRelationships between objects (one-to-many, many-to-many)
ViewMetadataSaved views with filters, sorts, group-by, column order
RoleMetadataPermission roles with field-level access control

Why It Matters

When a user creates a custom "Vendors" object with fields like "Contract Value" and "Renewal Date," the metadata engine: (1) stores the definition in the metadata tables, (2) creates the actual PostgreSQL table, (3) regenerates the GraphQL schema to include the new object, and (4) makes it available in the UI. No code deployment needed — it's all data-driven.

Database Architecture

Multi-Tenant Design

Twenty uses schema-based multi-tenancy in PostgreSQL. Each workspace gets its own database schema:

PostgreSQL Database
  ├── core schema                ← shared across all tenants
  │     ├── workspace            (workspace registry)
  │     ├── user                 (user accounts)
  │     ├── api_key              (API key tokens)
  │     ├── billing_*            (subscription data)
  │     └── ...

  ├── workspace_<uuid_1> schema  ← tenant A's data
  │     ├── company              (standard object)
  │     ├── person               (standard object)
  │     ├── opportunity
  │     ├── _vendor              (custom object)
  │     └── ...

  └── workspace_<uuid_2> schema  ← tenant B's data
        ├── company
        ├── person
        └── ...

For PMs: Why Schema-Based Multi-Tenancy?

Each customer's data lives in a completely separate database schema. This means: (1) one customer can never accidentally see another's data, (2) custom objects only exist in the workspace that created them, and (3) a misbehaving query on one workspace can't scan another's data. It's stronger isolation than row-level filtering.

Core Entity Models

EntityKey FieldsPurpose
Companyname, domainName, employees, annualRecurringRevenue, addressOrganizations being tracked
Personname (first/last), emails, phones, city, jobTitle, company (relation)Individual contacts
Opportunityname, stage, amount, closeDate, company, pointOfContactSales pipeline deals
Tasktitle, body, status, dueAt, assigneeAction items
Notetitle, body, positionFree-form notes
Messagesubject, text, headerMessageId, direction, messageThreadSynced emails
CalendarEventtitle, startsAt, endsAt, location, isCanceled, calendarChannelSynced calendar events
Workflowname, statuses, versions, runsAutomation definitions
Attachmentname, fullPath, type, authorFile attachments
WorkspaceMembername, avatar, colorScheme, locale, userEmailTeam members

Base Entity Pattern

Every workspace entity inherits common fields:

FieldTypePurpose
idUUIDPrimary key
createdAtDateTimeAuto-set on creation
updatedAtDateTimeAuto-set on update
deletedAtDateTime?Soft-delete timestamp
createdByRelationActor who created the record
positionFloatManual ordering in lists

API Architecture

Dual API: GraphQL + REST

Twenty exposes the same data through both GraphQL and REST — clients choose based on their needs.

GraphQL Endpoints:

/graphql     ← workspace data (companies, people, opportunities, custom objects)
/metadata    ← schema configuration (object/field definitions, views, roles)

REST Endpoints:

/rest/companies          ← CRUD for companies
/rest/people             ← CRUD for people
/rest/opportunities      ← CRUD for opportunities
/rest/<custom-object>    ← auto-generated for custom objects
/rest/batch/<object>     ← batch operations

GraphQL Schema Generation

The GraphQL schema is generated at runtime from metadata:

SourceOutput
Standard object workspace entitiesTyped queries, mutations, connections
Custom object metadataDynamic queries, mutations, connections
Relation metadataNested resolvers for related objects
Field metadataTyped fields with filtering and sorting

Query Capabilities

FeatureDescription
FilteringComplex AND/OR expressions on any field
SortingMulti-field sort with direction
PaginationCursor-based pagination
GroupingGroup-by for kanban and summary views
AggregationCount, sum, average on numeric fields
Nested queriesTraverse relationships in a single request
SubscriptionsReal-time updates via Server-Sent Events

Authentication Architecture

Auth Methods

MethodUse Case
JWTPrimary — access tokens for API and frontend sessions
Google OAuthSocial login via Google accounts
Microsoft OAuthSocial login via Microsoft accounts
SSO (SAML)Enterprise single sign-on
SSO (OIDC)OpenID Connect for enterprise IdPs
API KeysProgrammatic access for integrations

Token Types

TokenLifetimePurpose
Access TokenShort-livedWorkspace-scoped API access
Refresh TokenLong-livedRenew access tokens
Login TokenOne-timeInitial authentication exchange
API Key TokenPermanent (until revoked)Machine-to-machine access
Application TokenVariableThird-party app access

Permission Model

  • Workspace roles — Admin, Member, Guest with configurable permissions
  • Object-level permissions — Read, Create, Update, Delete per object type
  • Field-level permissions — Visibility and editability per field
  • Custom roles — Define granular permission sets

Real-Time Architecture

Twenty uses Server-Sent Events (SSE) for real-time database change notifications:

Browser                         Server
  │                                │
  │  SSE Connection (/graphql)     │
  │ ──────────────────────────────►│
  │                                │
  │                                │  User B updates a Company
  │                                │       │
  │                                │  PostgreSQL notifies NestJS
  │                                │       │
  │                                │  Redis pub/sub broadcasts
  │                                │       │
  │  ◄── SSE Event: {              │
  │        object: "company",      │
  │        action: "update",       │
  │        recordId: "uuid"        │
  │      }                         │
  │                                │
  │  Apollo cache invalidation     │
  │  → UI re-renders               │

Why SSE Over WebSockets?

Server-Sent Events are simpler than WebSockets — they work over standard HTTP, pass through proxies and load balancers without special configuration, and automatically reconnect. Since Twenty only needs server-to-client notifications (not bidirectional communication), SSE is the perfect fit.

Background Job Architecture

BullMQ processes background work through 15+ named queues:

QueuePurpose
messaging-queueEmail sync (Gmail/Microsoft import, participant matching)
calendar-queueCalendar event sync
workflow-queueWorkflow execution (triggers, actions, delays)
webhook-queueWebhook delivery to external endpoints
email-queueTransactional email sending
cron-queueScheduled jobs (cleanup, sync polling, analytics)
ai-queueAI-powered enrichment and summarization
contact-creation-queueAuto-create companies/contacts from emails
entity-events-to-db-queueTimeline/activity persistence
delete-cascade-queueCascading record deletion
delayed-jobs-queueWorkflow delay steps

What's Next