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 Entity | What It Stores |
|---|---|
| ObjectMetadata | Object name, label, description, icon, permissions |
| FieldMetadata | Field name, type (text, number, currency, relation, etc.), validation |
| RelationMetadata | Relationships between objects (one-to-many, many-to-many) |
| ViewMetadata | Saved views with filters, sorts, group-by, column order |
| RoleMetadata | Permission 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
| Entity | Key Fields | Purpose |
|---|---|---|
| Company | name, domainName, employees, annualRecurringRevenue, address | Organizations being tracked |
| Person | name (first/last), emails, phones, city, jobTitle, company (relation) | Individual contacts |
| Opportunity | name, stage, amount, closeDate, company, pointOfContact | Sales pipeline deals |
| Task | title, body, status, dueAt, assignee | Action items |
| Note | title, body, position | Free-form notes |
| Message | subject, text, headerMessageId, direction, messageThread | Synced emails |
| CalendarEvent | title, startsAt, endsAt, location, isCanceled, calendarChannel | Synced calendar events |
| Workflow | name, statuses, versions, runs | Automation definitions |
| Attachment | name, fullPath, type, author | File attachments |
| WorkspaceMember | name, avatar, colorScheme, locale, userEmail | Team members |
Base Entity Pattern
Every workspace entity inherits common fields:
| Field | Type | Purpose |
|---|---|---|
id | UUID | Primary key |
createdAt | DateTime | Auto-set on creation |
updatedAt | DateTime | Auto-set on update |
deletedAt | DateTime? | Soft-delete timestamp |
createdBy | Relation | Actor who created the record |
position | Float | Manual 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 operationsGraphQL Schema Generation
The GraphQL schema is generated at runtime from metadata:
| Source | Output |
|---|---|
| Standard object workspace entities | Typed queries, mutations, connections |
| Custom object metadata | Dynamic queries, mutations, connections |
| Relation metadata | Nested resolvers for related objects |
| Field metadata | Typed fields with filtering and sorting |
Query Capabilities
| Feature | Description |
|---|---|
| Filtering | Complex AND/OR expressions on any field |
| Sorting | Multi-field sort with direction |
| Pagination | Cursor-based pagination |
| Grouping | Group-by for kanban and summary views |
| Aggregation | Count, sum, average on numeric fields |
| Nested queries | Traverse relationships in a single request |
| Subscriptions | Real-time updates via Server-Sent Events |
Authentication Architecture
Auth Methods
| Method | Use Case |
|---|---|
| JWT | Primary — access tokens for API and frontend sessions |
| Google OAuth | Social login via Google accounts |
| Microsoft OAuth | Social login via Microsoft accounts |
| SSO (SAML) | Enterprise single sign-on |
| SSO (OIDC) | OpenID Connect for enterprise IdPs |
| API Keys | Programmatic access for integrations |
Token Types
| Token | Lifetime | Purpose |
|---|---|---|
| Access Token | Short-lived | Workspace-scoped API access |
| Refresh Token | Long-lived | Renew access tokens |
| Login Token | One-time | Initial authentication exchange |
| API Key Token | Permanent (until revoked) | Machine-to-machine access |
| Application Token | Variable | Third-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:
| Queue | Purpose |
|---|---|
messaging-queue | Email sync (Gmail/Microsoft import, participant matching) |
calendar-queue | Calendar event sync |
workflow-queue | Workflow execution (triggers, actions, delays) |
webhook-queue | Webhook delivery to external endpoints |
email-queue | Transactional email sending |
cron-queue | Scheduled jobs (cleanup, sync polling, analytics) |
ai-queue | AI-powered enrichment and summarization |
contact-creation-queue | Auto-create companies/contacts from emails |
entity-events-to-db-queue | Timeline/activity persistence |
delete-cascade-queue | Cascading record deletion |
delayed-jobs-queue | Workflow delay steps |