Agent-readable wiki

Twenty CRM — Developer Reference Wiki

Twenty is an open-source, self-hostable CRM built as an Nx monorepo with a NestJS/GraphQL backend, React/Jotai frontend, and a multi-tenant metadata-driven schema engine. This reference covers architecture, core modules, data flows, and operational surfaces for contributors and integrators.

Pages

  1. Technical OrientationRepository layout, Nx workspace conventions, tech stack, key entry points (main.ts, app.module.ts, index.tsx), and how the backend, frontend, and shared packages relate to each other. Starting point for any new contributor.
  2. Metadata Engine & Multi-Tenant SchemaThe heart of Twenty's flexibility: the metadata engine that drives dynamic object definitions, field types, relations, and per-workspace GraphQL schema generation. Covers TwentyORM, workspace datasource, metadata modules (object-metadata, field-metadata, index-metadata), and the workspace cache layer.
  3. Core NestJS Modules & API SurfaceServer-side domain modules: auth (JWT, SSO), billing, messaging, calendar, workflow, workspace, and the GraphQL API layers (code-first resolvers, subscriptions, dataloaders). How NestJS modules are registered in app.module.ts and how the GraphQL Yoga gateway is configured.
  4. Workspace Runtime, Jobs & Instance CommandsBullMQ background worker, queue-worker bootstrapping, database migration commands (RegisteredInstanceCommand / RegisteredWorkspaceCommand), upgrade pattern (fast vs slow instance commands), and the workspace event emitter that drives real-time side effects.
  5. Frontend Module Map & State ArchitectureReact 18 / Jotai / Apollo Client architecture: how modules (object-record, settings, auth, workflow, command-menu, navigation) are structured, how global state atoms compose with GraphQL cache, and the Linaria/Lingui conventions. Covers the twenty-ui shared component library and the metadata-store that mirrors backend schema on the client.
  6. Integrations, Extension Points & Operational SurfacesExternal integration surfaces: Zapier connector, REST/GraphQL client SDK (twenty-client-sdk), email templates (twenty-emails), connected-account sync (messaging, calendar), AI/code-interpreter modules, and the Docker Compose dev/prod setup. How to extend Twenty with new workspace modules, custom objects, and external connectors.

Complete Markdown

# Twenty CRM — Developer Reference Wiki

> Twenty is an open-source, self-hostable CRM built as an Nx monorepo with a NestJS/GraphQL backend, React/Jotai frontend, and a multi-tenant metadata-driven schema engine. This reference covers architecture, core modules, data flows, and operational surfaces for contributors and integrators.

## Context Links

- [Agent index](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/llms.txt)
- [Human interactive wiki](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6)
- [GitHub repository](https://github.com/twentyhq/twenty)

## Repository Metadata

- Repository: twentyhq/twenty

- Generated: 2026-05-27T05:38:42.694Z
- Updated: 2026-05-29T04:34:33.990Z
- Runtime: Claude Code
- Format: Technical
- Pages: 6

## Page Index

- 01. [Technical Orientation](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/01-technical-orientation.md) - Repository layout, Nx workspace conventions, tech stack, key entry points (main.ts, app.module.ts, index.tsx), and how the backend, frontend, and shared packages relate to each other. Starting point for any new contributor.
- 02. [Metadata Engine & Multi-Tenant Schema](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/02-metadata-engine-multi-tenant-schema.md) - The heart of Twenty's flexibility: the metadata engine that drives dynamic object definitions, field types, relations, and per-workspace GraphQL schema generation. Covers TwentyORM, workspace datasource, metadata modules (object-metadata, field-metadata, index-metadata), and the workspace cache layer.
- 03. [Core NestJS Modules & API Surface](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/03-core-nestjs-modules-api-surface.md) - Server-side domain modules: auth (JWT, SSO), billing, messaging, calendar, workflow, workspace, and the GraphQL API layers (code-first resolvers, subscriptions, dataloaders). How NestJS modules are registered in app.module.ts and how the GraphQL Yoga gateway is configured.
- 04. [Workspace Runtime, Jobs & Instance Commands](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/04-workspace-runtime-jobs-instance-commands.md) - BullMQ background worker, queue-worker bootstrapping, database migration commands (RegisteredInstanceCommand / RegisteredWorkspaceCommand), upgrade pattern (fast vs slow instance commands), and the workspace event emitter that drives real-time side effects.
- 05. [Frontend Module Map & State Architecture](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/05-frontend-module-map-state-architecture.md) - React 18 / Jotai / Apollo Client architecture: how modules (object-record, settings, auth, workflow, command-menu, navigation) are structured, how global state atoms compose with GraphQL cache, and the Linaria/Lingui conventions. Covers the twenty-ui shared component library and the metadata-store that mirrors backend schema on the client.
- 06. [Integrations, Extension Points & Operational Surfaces](https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/06-integrations-extension-points-operational-surfaces.md) - External integration surfaces: Zapier connector, REST/GraphQL client SDK (twenty-client-sdk), email templates (twenty-emails), connected-account sync (messaging, calendar), AI/code-interpreter modules, and the Docker Compose dev/prod setup. How to extend Twenty with new workspace modules, custom objects, and external connectors.

## Source File Index

- `CLAUDE.md`
- `nx.json`
- `package.json`
- `packages/twenty-client-sdk`
- `packages/twenty-docker`
- `packages/twenty-e2e-testing`
- `packages/twenty-emails/src`
- `packages/twenty-front/src/index.tsx`
- `packages/twenty-front/src/modules/auth`
- `packages/twenty-front/src/modules/command-menu`
- `packages/twenty-front/src/modules/metadata-store`
- `packages/twenty-front/src/modules/navigation`
- `packages/twenty-front/src/modules/object-record`
- `packages/twenty-front/src/modules/settings`
- `packages/twenty-front/src/modules/workflow`
- `packages/twenty-server/docs/UPGRADE_COMMANDS.md`
- `packages/twenty-server/src/app.module.ts`
- `packages/twenty-server/src/command`
- `packages/twenty-server/src/database`
- `packages/twenty-server/src/engine/api`
- `packages/twenty-server/src/engine/core-entity-cache`
- `packages/twenty-server/src/engine/core-modules/ai`
- `packages/twenty-server/src/engine/core-modules/auth`
- `packages/twenty-server/src/engine/core-modules/billing`
- `packages/twenty-server/src/engine/core-modules/core-engine.module.ts`
- `packages/twenty-server/src/engine/dataloaders`
- `packages/twenty-server/src/engine/metadata-modules/field-metadata`
- `packages/twenty-server/src/engine/metadata-modules/object-metadata`
- `packages/twenty-server/src/engine/subscriptions`
- `packages/twenty-server/src/engine/twenty-orm`
- `packages/twenty-server/src/engine/workspace-cache`
- `packages/twenty-server/src/engine/workspace-cache-storage`
- `packages/twenty-server/src/engine/workspace-datasource`
- `packages/twenty-server/src/engine/workspace-event-emitter`
- `packages/twenty-server/src/engine/workspace-manager`
- `packages/twenty-server/src/main.ts`
- `packages/twenty-server/src/modules/connected-account`
- `packages/twenty-server/src/modules/messaging`
- `packages/twenty-server/src/modules/modules.module.ts`
- `packages/twenty-server/src/modules/workflow`
- `packages/twenty-server/src/queue-worker`
- `packages/twenty-ui/src`
- `packages/twenty-utils/setup-dev-env.sh`
- `packages/twenty-zapier`
- `README.md`
- `tsconfig.base.json`

---

## 01. Technical Orientation

> Repository layout, Nx workspace conventions, tech stack, key entry points (main.ts, app.module.ts, index.tsx), and how the backend, frontend, and shared packages relate to each other. Starting point for any new contributor.

- Page Markdown: https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/01-technical-orientation.md
- Generated: 2026-05-27T05:38:42.690Z

### Source Files

- `README.md`
- `CLAUDE.md`
- `nx.json`
- `package.json`
- `tsconfig.base.json`
- `packages/twenty-server/src/main.ts`
- `packages/twenty-front/src/index.tsx`
- `packages/twenty-server/src/app.module.ts`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/twenty-front/src/index.tsx](packages/twenty-front/src/index.tsx)
- [packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts](packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts)
- [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts](packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts)
- [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomFamilyState.ts](packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomFamilyState.ts)
- [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomWritableFamilySelector.ts](packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomWritableFamilySelector.ts)
- [packages/twenty-front/src/modules/object-record/record-store/states/recordStoreFamilyState.ts](packages/twenty-front/src/modules/object-record/record-store/states/recordStoreFamilyState.ts)
- [packages/twenty-front/src/modules/auth/states/tokenPairState.ts](packages/twenty-front/src/modules/auth/states/tokenPairState.ts)
- [packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts](packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts)
- [packages/twenty-front/src/modules/apollo/services/apollo.factory.ts](packages/twenty-front/src/modules/apollo/services/apollo.factory.ts)
- [packages/twenty-ui/src/theme-constants/themeCssVariables.ts](packages/twenty-ui/src/theme-constants/themeCssVariables.ts)
- [packages/twenty-ui/src/theme-constants/ThemeProvider.tsx](packages/twenty-ui/src/theme-constants/ThemeProvider.tsx)
- [packages/twenty-ui/package.json](packages/twenty-ui/package.json)
</details>

# Technical Orientation

This page is a contributor orientation for the **twenty-front** React application: how its ~55 feature modules are organized, how global state flows through a custom Jotai layer, how Apollo Client's GraphQL cache relates (and does not relate) to that state, and what conventions govern styling and internationalization. It also covers the `twenty-ui` shared component library and the `metadata-store` module that mirrors backend schema on the client.

Read this page before touching any frontend code. The patterns here—state factory functions, the metadata-store/Apollo split, the Linaria CSS variable system—appear everywhere and are easy to misunderstand from code alone.

---

## Module Directory Layout

All feature code lives under `packages/twenty-front/src/modules/`. There are approximately 55 modules; the table below covers the ones most likely to be entered first.

| Module | Responsibility |
|---|---|
| `apollo/` | ApolloClient factory, link chain, token renewal, optimistic effects |
| `auth/` | Authentication state, sign-in/up flows, token management |
| `command-menu/` | Cmd+K command palette; component-instance-scoped state |
| `context-store/` | Per-view contextual selection and focus state |
| `metadata-store/` | Client-side cache of all backend schema/config (Jotai, localStorage) |
| `navigation/` | Navigation drawer, last-visited tracking, mobile bar |
| `object-metadata/` | Object-definition hooks and derived selectors |
| `object-record/` | Record fetching, the Jotai record store, field editing |
| `settings/` | Workspace settings pages and their data layer |
| `ui/` | App-specific UI primitives, Jotai utilities, theme hooks |
| `views/` | View definitions, filters, sorts, groupings |
| `workflow/` | Workflow builder UI and state |

The `ui/utilities/state/jotai/` subtree is the cross-cutting infrastructure all other modules depend on for state. Understanding it is prerequisite to reading any module's `states/` directory.

---

## Jotai State Architecture

### The Single Global Store

One Jotai store is created at application startup and reset on logout:

```ts
// packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts
import { createStore } from 'jotai';
export let jotaiStore = createStore();
export const resetJotaiStore = () => {
  clearAllSessionLocalStorageKeys();
  jotaiStore = createStore();
  return jotaiStore;
};
```

Sources: [packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts:1-12]()

### Factory Functions — the Atom Abstraction Layer

Rather than calling `atom()` directly, all state is created through factory functions in `ui/utilities/state/jotai/utils/`. Each factory returns a typed wrapper object with a `.type` discriminant so that helper hooks can dispatch uniformly.

| Factory | Atom kind | Persistence options |
|---|---|---|
| `createAtomState` | Single writable atom | memory, localStorage, sessionStorage, cookie |
| `createAtomFamilyState` | Keyed Map-backed atom family | memory or localStorage |
| `createAtomSelector` | Read-only derived atom | — |
| `createAtomWritableFamilySelector` | Read-write derived family selector | — |
| `createAtomComponentState` / `createAtomComponentFamilyState` | Component-instance-scoped atoms | memory |

`createAtomState` uses `jotai/utils`'s `atomWithStorage` when a persistence backend is requested, and plain `atom()` otherwise. The cookie variant uses a custom `createJotaiCookieStorage` adapter.

Sources: [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts:27-40]()

### Canonical Atom Examples

**Simple persistent atom — auth token** (stored in a cookie):

```ts
// packages/twenty-front/src/modules/auth/states/tokenPairState.ts
export const tokenPairState = createAtomState<AuthTokenPair | null>({
  key: 'tokenPairState',
  defaultValue: null,
  useCookieStorage: {
    cookieKey: 'tokenPair',
    validateInitFn: (payload) => Boolean(payload['accessOrWorkspaceAgnosticToken']),
  },
});
```

Sources: [packages/twenty-front/src/modules/auth/states/tokenPairState.ts:4-12]()

**Keyed family atom — record store** (one slot per record ID):

```ts
// packages/twenty-front/src/modules/object-record/record-store/states/recordStoreFamilyState.ts
export const recordStoreFamilyState = createAtomFamilyState<
  ObjectRecord | null | undefined,
  string   // familyKey = recordId
>({ key: 'recordStoreFamilyState', defaultValue: null });
```

Sources: [packages/twenty-front/src/modules/object-record/record-store/states/recordStoreFamilyState.ts:4-10]()

**Writable family selector — single record field** (reads/writes into the family above):

```ts
export const recordStoreFamilySelector = createAtomWritableFamilySelector<
  unknown,
  { recordId: string; fieldName: string }
>({
  key: 'recordStoreFamilySelector',
  get: ({ recordId, fieldName }) => ({ get }) =>
    get(recordStoreFamilyState, recordId)?.[fieldName],
  set: ({ recordId, fieldName }) => ({ set }, newValue) =>
    set(recordStoreFamilyState, recordId, (prev) => ({ ...prev, [fieldName]: newValue })),
});
```

### State Hook Conventions

| Hook | Returns |
|---|---|
| `useAtomState(state)` | `[value, setter]` |
| `useAtomStateValue(state)` | Read-only value |
| `useSetAtomState(state)` | Write-only setter |
| `useAtomFamilyStateValue(state, key)` | Value for a family key |
| `useSetAtomFamilyState(state)` | `(key) => setter` |
| `useAtomComponentState(state)` | Component-instance-scoped `[value, setter]` |

These hooks all wrap Jotai's `useAtom` / `useAtomValue` / `useSetAtom` and dispatch on the wrapper object's `.type` field.

---

## The metadata-store Module

### What It Caches

`metadataStoreState` is a `createAtomFamilyState` keyed by `MetadataEntityKey` and persisted to **localStorage**. It stores 24 entity collections that mirror backend schema and configuration:

```ts
// packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts
export const ALL_METADATA_ENTITY_KEYS = [
  'objectMetadataItems', 'fieldMetadataItems', 'indexMetadataItems',
  'views', 'viewFields', 'viewFilters', 'viewSorts', 'viewGroups',
  'viewFilterGroups', 'viewFieldGroups',
  'pageLayouts', 'pageLayoutTabs', 'pageLayoutWidgets',
  'logicFunctions',
  'navigationMenuItems', 'commandMenuItems', 'frontComponents',
  'webhooks', 'roles', 'roleTargets',
  'rowLevelPermissionPredicates', 'rowLevelPermissionPredicateGroups',
  'agents', 'skills', 'agentChatThreads',
] as const;
```

Each entry is a `MetadataStoreItem` with `current`, `draft`, `status` (`'empty' | 'draft-pending' | 'up-to-date'`), and hash fields for cache validation.

Sources: [packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts:8-36]()

### Load and Sync Lifecycle

```text
App boot
  └─ MinimalMetadataLoadEffect (render-null effect component)
       ├─ calls useLoadMinimalMetadata()
       │    └─ Apollo client.query() [network-only] → FIND_MINIMAL_METADATA
       │         → compares returned collectionHashes vs. stored hashes
       │         → stale keys → useLoadStaleMetadataEntities() → full fetch
       │         → writes results into metadataStoreState Jotai atoms
       │              (Apollo cache is NOT used for storage)
       └─ MetadataStoreSSEEffect (listens to server-sent events)
            ├─ create/update → addToDraft({ key, items, collectionHash })
            ├─ delete → removeFromDraft({ key, itemIds, collectionHash })
            └─ applyChanges() → promotes draft → current
```

`useInvalidateMetadataStore()` clears all `currentCollectionHash` values, triggering a reload on next render.

### Apollo Cache vs. Jotai metadata-store

These two systems handle completely separate concerns and do not sync with each other:

| | Apollo `InMemoryCache` | Jotai `metadataStoreState` |
|---|---|---|
| **Caches** | Workspace record data (contacts, companies, etc.) | Schema / configuration (object definitions, views, roles, navigation) |
| **Fetch policy** | `cache-and-network` (default) | `network-only` (bypasses Apollo cache) |
| **Persistence** | In-memory only, reset on page reload | `localStorage`, survives reload |
| **Invalidation** | Apollo `cache.evict()` / optimistic effects | `useInvalidateMetadataStore()` |
| **SSE updates** | Optimistic effect patches on mutations | `MetadataStoreSSEEffect` applies drafts |

Apollo Client is used as a *transport* for metadata queries — `client.query()` sends the request, but the response is immediately written into Jotai atoms rather than stored in `InMemoryCache`.

---

## Apollo Client Configuration

### Link Chain

The `ApolloFactory` class (in `packages/twenty-front/src/modules/apollo/services/apollo.factory.ts`) builds a link pipeline:

```text
ErrorLink
  → AuthLink (setContext — injects Authorization, X-Schema-Version, X-App-Version headers)
  → [extraLinks]
  → [loggerLink in debug mode]
  → RetryLink (max 2 retries, 3 s initial delay; skips auth/413 errors)
  → StreamingRestLink
  → RestLink
  → UploadHttpLink
```

Sources: [packages/twenty-front/src/modules/apollo/services/apollo.factory.ts:1-44]()

### Token Renewal

A module-level `renewalPromise` variable is shared across all `ApolloFactory` instances so that concurrent `UNAUTHENTICATED` errors from the `/graphql` and `/metadata` clients collapse into a single renewal request (using RxJS `switchMap`). Max retries: 3, with 1 s backoff.

### Two Apollo Clients

| Client | Endpoint | Created in |
|---|---|---|
| Main (workspace data) | `/graphql` | `useApolloFactory` hook |
| Metadata | `/metadata` | `ApolloProvider` component, with captcha refresh link |

Both clients share the same token renewal logic through the module-level `renewalPromise`.

---

## Feature Module Patterns

### object-record

The record module owns the Jotai record store (`recordStoreFamilyState`, keyed by record ID), record-list fetching hooks, field editors, and optimistic mutation helpers. It is the largest module in the codebase.

Key internal shape:
- `record-store/states/` — atoms and selectors for individual record field access
- `record-filter/` — filter query builders
- `record-sort/` — sort state
- `hooks/` — `useCreateOneRecord`, `useUpdateOneRecord`, `useDeleteOneRecord`, etc.

### auth

All authentication state lives in `modules/auth/states/`. Most atoms are memory-only; `tokenPairState` is the only one persisted to a cookie. The auth module also owns `currentWorkspaceState`, `currentWorkspaceMemberState`, and `currentUserState`.

### navigation

Navigation state is minimal: `currentMobileNavigationDrawerState` (`'main' | 'settings'`), and two family atoms tracking last-visited object and view per object. Components include `AppNavigationDrawer`, `MainNavigationDrawer`, `SettingsNavigationDrawer`, and `MobileNavigationBar`.

### command-menu

Uses the **component instance context** pattern (`createComponentInstanceContext()`), allowing multiple independent palette instances scoped to different parts of the UI. State is not global — it is keyed to a context instance ID.

### workflow

The workflow builder module owns its own Jotai atoms for step selection, canvas position, and execution state. It follows the same `createAtomState` / `createAtomFamilyState` patterns as other modules.

### settings

Settings pages are grouped under `modules/settings/` with data fetched via Apollo and workspace configuration written back through mutations. Settings state is largely ephemeral (form state in component atoms) rather than global.

---

## twenty-ui Shared Component Library

`packages/twenty-ui` is the design system consumed by `twenty-front`. It is published as a private monorepo package with sub-path exports.

### Directory Structure

```text
packages/twenty-ui/src/
  display/       avatar, chip, icon, tag, tooltip, typography, status, callout …
  feedback/      loader, progress-bar
  input/         button, code-editor, color-scheme
  layout/        card, modal, section, animated-expandable-container
  navigation/    link, menu, navigation-bar, notification-counter
  theme-constants/  CSS variable references, ThemeProvider, generated CSS files
  utilities/     animation components, dimension helpers
  testing/       Storybook decorators
```

Sub-path imports are declared in `package.json`: `twenty-ui/display`, `twenty-ui/input`, `twenty-ui/layout`, `twenty-ui/navigation`, `twenty-ui/theme`, `twenty-ui/theme-constants`, etc.

### Linaria CSS-in-JS

Styling uses **`@linaria/react`** (zero-runtime CSS-in-JS via `wyw-in-js` toolchain). The import is always:

```ts
import { styled } from '@linaria/react';
```

All style values reference CSS custom properties via a `themeCssVariables` object:

```ts
// used in styled components throughout twenty-ui
import { themeCssVariables } from '@ui/theme-constants';

const StyledContainer = styled.div`
  display: flex;
  gap: ${themeCssVariables.spacing[4]};
  border-radius: ${themeCssVariables.border.radius.md};
`;
```

Dynamic props follow the standard Linaria pattern:

```ts
const StyledButton = styled.div<{ accent: 'default' | 'danger'; disabled: boolean }>`
  color: ${({ accent, disabled }) =>
    disabled ? themeCssVariables.font.color.tertiary :
    accent === 'danger' ? themeCssVariables.font.color.danger :
    themeCssVariables.font.color.secondary};
`;
```

The CSS custom property values themselves are defined in generated `.light` and `.dark` CSS files (`theme-light.css`, `theme-dark.css`). `ThemeProvider` toggles `class="light"` or `class="dark"` on `document.documentElement` to switch themes. Components that need raw numeric values read from `useContext(ThemeContext).theme` (populated via `getComputedStyle`).

Responsive breakpoints use the `MOBILE_VIEWPORT = 768` constant (not a CSS variable, because CSS variables cannot be used in media queries):

```ts
import { MOBILE_VIEWPORT } from 'twenty-ui/theme-constants';
// @media (max-width: ${MOBILE_VIEWPORT}px) { ... }
```

### CSS Import Order

`index.tsx` imports the global CSS files before rendering the React tree:

```ts
// packages/twenty-front/src/index.tsx
import 'twenty-ui/style.css';
import 'twenty-ui/theme-light.css';
import 'twenty-ui/theme-dark.css';
import './index.css';
```

Sources: [packages/twenty-front/src/index.tsx:5-8]()

---

## Lingui Internationalization

Lingui lives in `twenty-front`, not `twenty-ui`. Three usage patterns appear across the codebase:

**Pattern 1 — hook + tagged template** (most common in components):
```ts
import { useLingui } from '@lingui/react/macro';
const { t } = useLingui();
// in JSX:
<Button label={t`Save`} />
```

**Pattern 2 — `t` macro directly** (utility functions outside React):
```ts
import { t } from '@lingui/core/macro';
const errorMessage = t`Invalid cron expression`;
```

**Pattern 3 — `msg` + `i18n._()` for static descriptors** (evaluated lazily):
```ts
import { msg } from '@lingui/core/macro';
import { i18n } from '@lingui/core';
const label = msg`Default role`;
i18n._(label); // resolved at call site
```

**Locale detection order**: URL param `?locale=` → `localStorage['locale']` → `navigator.language` → English fallback. `dynamicActivate(locale)` lazy-imports the compiled catalog from `src/locales/generated/${locale}.ts`.

---

## Architecture Summary

```text
packages/twenty-front/src/
│
├── index.tsx                  Entry point — mounts <App />, imports CSS
├── app/components/App.tsx     Root component, React Router, providers
│
└── modules/
    ├── apollo/                ApolloFactory, link chain, two clients
    │     └── InMemoryCache   Caches workspace record query results
    │
    ├── metadata-store/        Jotai family state (localStorage)
    │     └── 24 entity keys  Schema, views, navigation, roles, AI agents …
    │         loaded by MinimalMetadataLoadEffect (Apollo as transport only)
    │         updated by MetadataStoreSSEEffect (SSE → draft → current)
    │
    ├── object-record/         recordStoreFamilyState (Jotai, memory)
    │     └── keyed by recordId; selectors drill to individual fields
    │
    ├── auth/                  tokenPairState (cookie), currentUser (memory)
    ├── navigation/            drawer state, last-visited atoms
    ├── command-menu/          component-instance-scoped state
    ├── workflow/              builder atoms, execution state
    ├── settings/              mostly ephemeral form state
    │
    └── ui/utilities/state/jotai/
          ├── jotaiStore.ts    Single global Jotai store, reset on logout
          └── utils/           createAtomState, createAtomFamilyState,
                               createAtomSelector, createAtomWritableFamilySelector,
                               createAtomComponentState, buildGetHelper …

packages/twenty-ui/
    ├── src/display|input|layout|navigation|…   Shared components (Linaria styled)
    └── src/theme-constants/   CSS variables, ThemeProvider, light/dark CSS
```

The central architectural insight: **Apollo `InMemoryCache` owns workspace record data; Jotai `metadataStoreState` owns schema/configuration.** These two caches are independent and require separate invalidation strategies. Apollo is used as a network transport for metadata, but the cache layer for metadata is entirely Jotai-backed and localStorage-persisted.

Sources: [packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts:1-36](), [packages/twenty-front/src/modules/ui/utilities/state/jotai/jotaiStore.ts:1-12](), [packages/twenty-front/src/modules/apollo/services/apollo.factory.ts:41-44]()

---

## 02. Metadata Engine & Multi-Tenant Schema

> The heart of Twenty's flexibility: the metadata engine that drives dynamic object definitions, field types, relations, and per-workspace GraphQL schema generation. Covers TwentyORM, workspace datasource, metadata modules (object-metadata, field-metadata, index-metadata), and the workspace cache layer.

- Page Markdown: https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/02-metadata-engine-multi-tenant-schema.md
- Generated: 2026-05-27T00:39:34.237Z

### Source Files

- `packages/twenty-server/src/engine/metadata-modules/object-metadata`
- `packages/twenty-server/src/engine/metadata-modules/field-metadata`
- `packages/twenty-server/src/engine/twenty-orm`
- `packages/twenty-server/src/engine/workspace-datasource`
- `packages/twenty-server/src/engine/workspace-cache`
- `packages/twenty-server/src/engine/workspace-cache-storage`
- `packages/twenty-server/src/engine/workspace-manager`
- `packages/twenty-server/src/engine/core-entity-cache`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts](packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts)
- [packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts](packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts)
- [packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts](packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts)
- [packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts](packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts)
- [packages/twenty-server/src/engine/workspace-datasource/utils/get-workspace-schema-name.util.ts](packages/twenty-server/src/engine/workspace-datasource/utils/get-workspace-schema-name.util.ts)
- [packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts](packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts)
- [packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts](packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts)
- [packages/twenty-server/src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service.ts](packages/twenty-server/src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service.ts)
- [packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts](packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts)
- [packages/twenty-server/src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service.ts](packages/twenty-server/src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service.ts)
- [packages/twenty-server/src/engine/workspace-manager/types/syncable-entity.interface.ts](packages/twenty-server/src/engine/workspace-manager/types/syncable-entity.interface.ts)
</details>

# Metadata Engine & Multi-Tenant Schema

Twenty's metadata engine is the core mechanism that allows each workspace to define its own object model at runtime — without requiring code changes or re-deployments. Every object type (e.g. `Company`, `Contact`, a custom `Deal`) is stored as a record in shared metadata tables, and its fields, relations, and indexes are likewise stored as metadata. The engine reads these records to dynamically generate PostgreSQL schemas, TypeORM `EntitySchema` definitions, and per-workspace GraphQL APIs on demand.

This page covers the full pipeline: from the canonical metadata entities stored in PostgreSQL, through the workspace datasource and TwentyORM layer that brings them alive as a queryable TypeORM data source, to the cache layer that keeps schema lookups sub-millisecond at runtime, and finally to the per-workspace GraphQL schema generation that exposes every object as a typed API.

---

## Multi-Tenant Schema Isolation

### Per-Workspace PostgreSQL Schema

Each workspace occupies its own PostgreSQL schema within the shared database. The schema name is derived deterministically from the workspace UUID:

```typescript
// packages/twenty-server/src/engine/workspace-datasource/utils/get-workspace-schema-name.util.ts
export const getWorkspaceSchemaName = (workspaceId: string): string => {
  return `workspace_${uuidToBase36(workspaceId)}`;
};
```

This produces stable, collision-free names like `workspace_3k2f9p1`. All workspace object tables live inside this schema; the core/metadata tables (users, workspaces, `objectMetadata`, `fieldMetadata`, etc.) remain in the default `public` schema.

### WorkspaceDataSourceService

`WorkspaceDataSourceService` manages the lifecycle of workspace schemas in PostgreSQL. It operates on the shared `coreDataSource` (TypeORM `DataSource` pointed at the main database) to create and drop schemas.

```typescript
// packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts:56-69
public async createWorkspaceDBSchema(workspaceId: string): Promise<string> {
  this.assertDDLNotLocked();
  const schemaName = getWorkspaceSchemaName(workspaceId);
  const queryRunner = this.coreDataSource.createQueryRunner();
  try {
    await queryRunner.createSchema(schemaName, true);
    return schemaName;
  } finally {
    await queryRunner.release();
  }
}
```

DDL changes can be globally locked via the `WORKSPACE_SCHEMA_DDL_LOCKED` config flag (used during hot upgrades), causing `createWorkspaceDBSchema` and `deleteWorkspaceDBSchema` to throw a `WorkspaceDataSourceException` with code `DDL_LOCKED`.

Sources: [packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts:30-38](packages/twenty-server/src/engine/workspace-datasource/workspace-datasource.service.ts)

---

## Core Metadata Entities

All metadata is stored as rows in shared PostgreSQL tables. Three primary entities form the schema definition:

### ObjectMetadataEntity

`ObjectMetadataEntity` is a TypeORM entity (table `objectMetadata`) that represents a single object type within a workspace. It is scoped to a `workspaceId` via `SyncableEntity` and carries:

| Column | Description |
|---|---|
| `nameSingular` / `namePlural` | API names, unique per workspace |
| `labelSingular` / `labelPlural` | Display labels |
| `isCustom` | `true` for user-created types |
| `isSystem` | Hidden from API, used internally |
| `isRemote` | Object backed by an external data source |
| `isSearchable` | Included in global full-text search |
| `isAuditLogged` | Changes written to audit log |
| `labelIdentifierFieldMetadataId` | Field used as the primary display label |
| `standardOverrides` | JSONB blob for overriding standard object properties |
| `duplicateCriteria` | JSONB rules for deduplication |

Unique constraints enforce that `(nameSingular, workspaceId)` and `(namePlural, workspaceId)` are each unique.

Sources: [packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts:22-147](packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.entity.ts)

### FieldMetadataEntity

`FieldMetadataEntity` (table `fieldMetadata`) represents one field on an object. It carries a generic type parameter `TFieldMetadataType` constraining the field's type to a value from the `FieldMetadataType` enum defined in `twenty-shared/types`.

Key columns:

| Column | Description |
|---|---|
| `type` | Field type (TEXT, NUMBER, RELATION, MORPH_RELATION, UUID, etc.) |
| `name` | Field name, unique per `(objectMetadataId, workspaceId)` |
| `defaultValue` | JSONB default, type-safe via `FieldMetadataDefaultValue<T>` |
| `options` | JSONB options (e.g. SELECT choices), type-safe via `FieldMetadataOptions<T>` |
| `settings` | JSONB settings (e.g. date format), type-safe via `FieldMetadataSettings<T>` |
| `relationTargetFieldMetadataId` | FK to the inverse side of a relation |
| `relationTargetObjectMetadataId` | Target object for relation fields |
| `morphId` | Required on `MORPH_RELATION` fields; references the morph discriminator |
| `isCustom` / `isSystem` / `isActive` | Lifecycle flags |
| `isUnique` | Derived at cache-build time from `IndexMetadata`; not stored as a column |
| `standardOverrides` | JSONB per-field label/description overrides |

A database-level `CHECK` constraint enforces that `MORPH_RELATION` fields must have a non-null `morphId`. The `isUnique` property is intentionally not persisted — it is derived during flat-entity cache construction from the presence of a single-field unique `IndexMetadata`, then attached to the flat representation for consumers.

Sources: [packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts:38-215](packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts)

### IndexMetadataEntity

`IndexMetadataEntity` (table `indexMetadata`) defines a database index on an object's underlying table. Each index references its parent `ObjectMetadataEntity` and carries one or more `IndexFieldMetadataEntity` join rows naming the fields to index.

Key properties: `isUnique` (unique constraint), `indexType` (`BTREE` by default, from the `IndexType` enum), and `indexWhereClause` for partial indexes.

Sources: [packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts:29-80](packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts)

### SyncableEntity — the Common Supertype

All three entities extend `SyncableEntity`, which in turn extends `WorkspaceRelatedEntity`. This base class carries the `workspaceId` foreign key that scopes each metadata record to a specific tenant, plus `universalIdentifier` and `applicationId` fields that support cross-workspace standard-object references and application-scoped objects.

---

## Flat-Entity Cache Layer

Querying raw metadata entities from the database on every API request would be prohibitively expensive. Twenty introduces a **flat-entity cache** that serializes the denormalized (joined) view of metadata into Redis and serves it with a simple version check.

### Cache Structure

The cache stores several parallel "flat maps" per workspace, each keyed on the metadata version number:

| Flat Map Key | Contents |
|---|---|
| `flatObjectMetadataMaps` | Object metadata indexed by id, by nameSingular, by namePlural |
| `flatFieldMetadataMaps` | Field metadata indexed by id, by object |
| `flatIndexMaps` | Index metadata indexed by id, by object |
| `flatApplicationMaps` | Application/integration scopes |

`FlatEntityMaps<T>` is a generic container that holds an `idByX` lookup map plus a flat `byId` record store, enabling O(1) access by any indexed key.

### WorkspaceCacheStorageService

`WorkspaceCacheStorageService` is the low-level cache access layer backed by Redis. It stores and retrieves the serialized flat maps and the workspace's `metadataVersion` integer. The version acts as the cache invalidation key.

### WorkspaceManyOrAllFlatEntityMapsCacheService

This service wraps `WorkspaceCacheStorageService` and adds a lazy-recompute strategy via `GetDataFromCacheWithRecomputeService`. On a cache miss it triggers a full recompute, writes results back to Redis, then serves the fresh data. Callers request only the keys they need (e.g. `['flatObjectMetadataMaps', 'flatFieldMetadataMaps']`), avoiding unnecessary deserialization.

```typescript
// packages/twenty-server/src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service.ts:23-93
getFromCacheWithRecompute = async ({ workspaceId, getCacheData,
  getCacheVersion, recomputeCache, ... }) => {
  cachedVersion = await getCacheVersion(workspaceId);
  if (isDefined(cachedVersion)) {
    const cacheKey = `${workspaceId}-${cachedVersion}`;
    const cachedValue = this.cache.get(cacheKey); // in-process Map
    if (cachedValue) return cachedValue;
  }
  cachedData = await getCacheData(workspaceId);
  if (!isDefined(cachedData) || !isDefined(cachedVersion)) {
    await recomputeCache({ workspaceId }); // write to Redis
    ...
  }
  this.cache.set(cacheKey, { version, data }); // populate in-process map
  return { version, data };
};
```

There is a two-tier caching strategy: an in-process `Map` (keyed by `workspaceId-version`) sits in front of Redis. Cache hits in the in-process tier require no network round-trips at all.

Sources: [packages/twenty-server/src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service.ts:23-93](packages/twenty-server/src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service.ts)

---

## TwentyORM — Dynamic TypeORM Data Source

Standard TypeORM requires entity classes known at compile time. Twenty extends this with a dynamic `EntitySchema`-based approach through **TwentyORM**, allowing runtime objects (from `ObjectMetadataEntity` rows) to be queried via TypeORM in the correct workspace PostgreSQL schema.

### TwentyORMModule

`TwentyORMModule` is declared `@Global()` and bootstraps the ORM layer. It imports:
- `WorkspaceCacheStorageModule` — Redis-backed flat-map persistence
- `WorkspaceManyOrAllFlatEntityMapsCacheModule` — lazy-recompute cache service
- `WorkspaceCacheModule` — higher-level workspace cache (roles, permissions)
- `PermissionsModule`, `WorkspaceFeatureFlagsMapCacheModule`, `FeatureFlagModule`

It exports `EntitySchemaFactory`, which builds TypeORM `EntitySchema` instances from `FlatObjectMetadata` at request time, targeting the correct workspace schema name.

Sources: [packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts:1-36](packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts)

### EntitySchemaFactory

`EntitySchemaFactory` (exported from `TwentyORMModule`) translates a `FlatObjectMetadata` record — retrieved from the flat-entity cache — into a TypeORM `EntitySchema`. It sets the target PostgreSQL schema to `workspace_<base36(workspaceId)>`, maps each `FlatFieldMetadata` entry to a TypeORM column definition, and wires up relations. The result is a fully qualified TypeORM schema that can be used with a workspace-scoped `DataSource`.

---

## Per-Workspace GraphQL Schema Generation

Each authenticated request to the workspace GraphQL endpoint operates against a dynamically generated `GraphQLSchema` derived from the workspace's current metadata.

### WorkspaceGraphQLSchemaGenerator

`WorkspaceGraphQLSchemaGenerator` orchestrates GraphQL schema generation. It delegates to `GqlTypeGenerator`, which traverses the flat object/field metadata maps and builds all the types, queries, and mutations:

```typescript
// packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts:18-52
async generateSchema(context: SchemaGenerationContext): Promise<GraphQLSchema> {
  const gqlTypesStorage = await this.gqlTypeGenerator.buildAndStore(context);

  const queryType = gqlTypesStorage.getGqlTypeByKey<GraphQLObjectType>(GqlOperation.Query);
  const mutationType = gqlTypesStorage.getGqlTypeByKey<GraphQLObjectType>(GqlOperation.Mutation);
  // throws if either is missing

  return new GraphQLSchema({
    query: queryType,
    mutation: mutationType,
    types: gqlTypesStorage.getAllGqlTypesExcept([GqlOperation.Query, GqlOperation.Mutation]),
  });
}
```

Sources: [packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts:18-52](packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory.ts)

### WorkspaceGraphqlSchemaSDLService

This service wraps the schema generator with SDL (Schema Definition Language) caching. Its `getOrComputeSchemaSDL` method:
1. Checks that the workspace has a valid `databaseSchema` string.
2. Calls `WorkspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps` to load the four flat maps.
3. Optionally filters objects and fields to the scope of a specific `applicationId` (using `flatApplicationMaps`).
4. Calls `WorkspaceGraphQLSchemaGenerator.generateSchema` with a `SchemaGenerationContext` built from the filtered maps.
5. Returns the SDL string plus metadata about used scalars and the maps themselves.

```typescript
// packages/twenty-server/src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service.ts:23-29
export type WorkspaceGraphqlSchemaSDLResult = {
  sdl: string;
  usedScalarNames: string[];
  flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
  flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
};
```

Sources: [packages/twenty-server/src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service.ts:23-100](packages/twenty-server/src/engine/api/graphql/workspace-graphql-schema-sdl/workspace-graphql-schema-sdl.service.ts)

---

## Data Flow: Request-Time Schema Resolution

```text
Authenticated API request
        │
        ▼
 AccessTokenService.validateTokenByRequest()
        │  → resolves workspace + metadataVersion
        ▼
 WorkspaceCacheStorageService.getMetadataVersion(workspaceId)
        │  → on miss: seeds version from workspace.metadataVersion
        ▼
 WorkspaceManyOrAllFlatEntityMapsCacheService
   .getOrRecomputeManyOrAllFlatEntityMaps(...)
        │  → in-process Map hit → return (no I/O)
        │  → Redis hit           → return (one network call)
        │  → miss                → recomputeCache() → Redis → return
        ▼
 FlatEntityMaps<FlatObjectMetadata>
 FlatEntityMaps<FlatFieldMetadata>
 FlatEntityMaps<FlatIndexMetadata>
        │
        ├─[GraphQL path]──▶ WorkspaceGraphQLSchemaGenerator.generateSchema()
        │                        → GqlTypeGenerator.buildAndStore(context)
        │                        → GraphQLSchema (query + mutation + types)
        │
        └─[REST path]────▶ EntitySchemaFactory (TwentyORM)
                                → TypeORM EntitySchema for workspace schema
                                → workspace DataSource query execution
```

---

## WorkspaceManagerModule — Workspace Lifecycle

`WorkspaceManagerModule` orchestrates workspace provisioning. It wires together:
- `WorkspaceDataSourceModule` — PostgreSQL schema creation/deletion
- `WorkspaceMigrationModule` — DDL migration runner
- `ObjectMetadataModule` — seeding standard objects
- `PermissionsModule`, `RoleModule`, `UserRoleModule` — default roles
- `TwentyStandardApplicationModule` — standard application objects

When a new workspace is created, `WorkspaceManagerService` calls `WorkspaceDataSourceService.createWorkspaceDBSchema`, seeds standard objects (via `ObjectMetadataModule`), runs DDL migrations to materialize tables in the new schema, and sets up default roles and permissions.

Sources: [packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts:25-49](packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts)

---

## Field Type System

Field types are defined as string literals in `twenty-shared/types` via `FieldMetadataType`. Representative types:

| Type | Storage | Notes |
|---|---|---|
| `TEXT` | `varchar` | Basic single-line text |
| `RICH_TEXT` | `jsonb` | Structured rich-text blocks |
| `NUMBER` | `float` | Numeric values |
| `BOOLEAN` | `boolean` | |
| `DATE_TIME` / `DATE` | `timestamptz` / `date` | |
| `SELECT` / `MULTI_SELECT` | `varchar` / `varchar[]` | Options stored in `fieldMetadata.options` JSONB |
| `RELATION` | — | FK resolved via `relationTargetFieldMetadataId` |
| `MORPH_RELATION` | — | Polymorphic relation; requires `morphId` |
| `UUID` | `uuid` | |
| `CURRENCY` | `jsonb` | Composite: amount + currency code |
| `ADDRESS` | `jsonb` | Composite: street, city, country, etc. |
| `LINKS` | `jsonb` | Composite: primary link + secondary links |
| `ACTOR` | `jsonb` | Composite: name + source + workspace member |

Composite ("composite") field types are stored as JSONB and their sub-fields are materialized as virtual fields during schema generation. The `isEnumFieldMetadataType` utility classifies SELECT/MULTI_SELECT for special GraphQL enum handling during schema build.

---

## Summary

Twenty's metadata engine achieves full per-workspace schema flexibility by storing object and field definitions as relational data, deriving PostgreSQL schemas dynamically using `workspace_<base36id>` namespacing, and keeping runtime overhead low through a two-tier (in-process + Redis) flat-entity cache. `TwentyORM` bridges the gap between dynamic metadata and TypeORM by generating `EntitySchema` objects at request time. The GraphQL layer reads the same flat maps to produce a complete, typed workspace API without static code generation. Any change to an object or field triggers a metadata version bump, invalidating the cache and causing the next request to recompute a fresh schema view.

---

## 03. Core NestJS Modules & API Surface

> Server-side domain modules: auth (JWT, SSO), billing, messaging, calendar, workflow, workspace, and the GraphQL API layers (code-first resolvers, subscriptions, dataloaders). How NestJS modules are registered in app.module.ts and how the GraphQL Yoga gateway is configured.

- Page Markdown: https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/03-core-nestjs-modules-api-surface.md
- Generated: 2026-05-27T00:36:50.063Z

### Source Files

- `packages/twenty-server/src/engine/core-modules/core-engine.module.ts`
- `packages/twenty-server/src/engine/core-modules/auth`
- `packages/twenty-server/src/engine/core-modules/billing`
- `packages/twenty-server/src/modules/messaging`
- `packages/twenty-server/src/modules/workflow`
- `packages/twenty-server/src/engine/api`
- `packages/twenty-server/src/engine/subscriptions`
- `packages/twenty-server/src/engine/dataloaders`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/twenty-server/src/app.module.ts](packages/twenty-server/src/app.module.ts)
- [packages/twenty-server/src/engine/core-modules/core-engine.module.ts](packages/twenty-server/src/engine/core-modules/core-engine.module.ts)
- [packages/twenty-server/src/engine/core-modules/auth/auth.module.ts](packages/twenty-server/src/engine/core-modules/auth/auth.module.ts)
- [packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts](packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts)
- [packages/twenty-server/src/engine/core-modules/billing/billing.module.ts](packages/twenty-server/src/engine/core-modules/billing/billing.module.ts)
- [packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts](packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts)
- [packages/twenty-server/src/engine/core-modules/sso/sso.module.ts](packages/twenty-server/src/engine/core-modules/sso/sso.module.ts)
- [packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts](packages/twenty-server/src/engine/api/graphql/core-graphql-api.module.ts)
- [packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts](packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts)
- [packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.module.ts](packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.module.ts)
- [packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts](packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts)
- [packages/twenty-server/src/engine/subscriptions/subscriptions.module.ts](packages/twenty-server/src/engine/subscriptions/subscriptions.module.ts)
- [packages/twenty-server/src/engine/dataloaders/dataloader.module.ts](packages/twenty-server/src/engine/dataloaders/dataloader.module.ts)
- [packages/twenty-server/src/engine/dataloaders/dataloader.service.ts](packages/twenty-server/src/engine/dataloaders/dataloader.service.ts)
</details>

# Core NestJS Modules & API Surface

The Twenty server backend is a NestJS application organized around a two-tier module hierarchy: a root `AppModule` that wires together infrastructure concerns, and a large `CoreEngineModule` that aggregates all domain-specific feature modules. Understanding this structure is essential for adding new server capabilities, debugging request flows, and navigating the GraphQL and REST API layers.

This page covers how domain modules (auth, billing, messaging, calendar, workflow, workspace, SSO) are declared and composed, how the three GraphQL Yoga endpoints are registered and configured, what Yoga plugins are active, how subscriptions and dataloaders integrate, and where REST and MCP API surface are defined.

---

## Module Hierarchy Overview

```text
AppModule  (packages/twenty-server/src/app.module.ts)
├── GraphQLModule (Yoga) × 3  — core / metadata / admin-panel
├── TwentyORMModule            — custom TypeORM integration
├── CoreEngineModule           — all domain feature modules
├── ModulesModule              — business-logic workspace modules
├── CoreGraphQLApiModule       — workspace schema factory + SDL
├── MetadataGraphQLApiModule   — metadata schema endpoint
├── AdminPanelGraphQLApiModule — admin schema endpoint
├── RestApiModule              — REST API surface
├── McpModule                  — MCP protocol endpoint
├── DataloaderModule           — per-request DataLoaders
├── SubscriptionsModule        — (via CoreEngineModule)
└── MiddlewareModule           — request middleware
```

`CoreEngineModule` is the central aggregator; it has no business logic of its own — it just imports and re-exports all domain modules.

---

## AppModule: Root Wiring

`AppModule` (`packages/twenty-server/src/app.module.ts`) is the NestJS entry point. Its key responsibilities:

### GraphQL Yoga Gateway Registration

Three independent `GraphQLModule.forRootAsync` calls register three separate GraphQL Yoga endpoints, each with its own path, schema, and plugin set:

| Module | Path | Schema scope |
|---|---|---|
| `GraphQLModule` in `AppModule` | `/graphql` | Core (resolvers from `CoreEngineModule`) |
| `MetadataGraphQLApiModule` | `/metadata` | Metadata (object/field definitions) |
| `AdminPanelGraphQLApiModule` | `/admin-panel` | Admin panel |

The core `/graphql` endpoint is configured via `GraphQLConfigService` using the `YogaDriver`:

```typescript
// packages/twenty-server/src/app.module.ts:52-56
GraphQLModule.forRootAsync<YogaDriverConfig>({
  driver: YogaDriver,
  imports: [GraphQLConfigModule, MetricsModule, DataloaderModule],
  useClass: GraphQLConfigService,
}),
```

Sources: [packages/twenty-server/src/app.module.ts:52-56]()

### Middleware Pipeline

`AppModule.configure()` applies two key middleware chains:

1. **GraphQL/Metadata/Admin routes** (`/graphql`, `/metadata`, `/admin-panel`): `GraphQLHydrateRequestFromTokenMiddleware` → `WorkspaceAuthContextMiddleware`. The hydrate middleware extracts the JWT and attaches user + workspace to `req`; the auth-context middleware validates workspace activation status.
2. **REST routes** (`/rest/*path`): `RestCoreMiddleware` → `WorkspaceAuthContextMiddleware`.
3. **MCP route** (`/mcp`): `McpMethodGuardMiddleware` only.

Sources: [packages/twenty-server/src/app.module.ts:114-145]()

---

## GraphQL Yoga Configuration

`GraphQLConfigService` (`packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts`) implements `GqlOptionsFactory<YogaDriverConfig>` and is the single source of truth for the core GraphQL endpoint's behavior.

### Yoga Plugins

The following plugins are always active:

| Plugin | Purpose |
|---|---|
| `useDirectExecution` | Short-circuits resolution for internally issued queries (avoids HTTP round-trips) |
| `useGraphQLErrorHandlerHook` | Normalizes and forwards errors to metrics + exception handler |
| `useDisableIntrospectionAndSuggestionsForUnauthenticatedUsers` | Blocks schema introspection in production for unauthenticated callers |
| `useValidateGraphqlQueryComplexity` | Enforces `GRAPHQL_MAX_FIELDS` and `GRAPHQL_MAX_ROOT_RESOLVERS` limits; rejects duplicate root resolvers |
| `useSentryTracing` | Added conditionally when Sentry is initialized |

The schema is built in code-first mode with `autoSchemaFile: true`, scoped to `CoreEngineModule` resolvers via `include: [CoreEngineModule]` and `resolverSchemaScope: 'core'`. The `JSON` scalar is registered globally.

Per-request DataLoaders are injected into the GraphQL context through the context factory:

```typescript
// packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts
context: () => ({
  loaders: this.dataloaderService.createLoaders(),
}),
```

In development mode, an Apollo Playground renderer is mounted at the endpoint.

Sources: [packages/twenty-server/src/engine/api/graphql/graphql-config/graphql-config.service.ts:40-95]()

### Metadata GraphQL Endpoint

`MetadataGraphQLApiModule` registers a second Yoga instance that serves the metadata schema (object metadata, field metadata, relations). It uses the same `YogaDriver` and reuses `DataloaderModule` and `MetricsModule`. The factory function is `metadataModuleFactory`. The metadata schema includes resolvers from `MetadataEngineModule`.

Sources: [packages/twenty-server/src/engine/api/graphql/metadata-graphql-api.module.ts]()

---

## CoreEngineModule: Domain Module Registry

`CoreEngineModule` (`packages/twenty-server/src/engine/core-modules/core-engine.module.ts`) imports ~65 modules and re-exports a smaller set for consumption by peer modules. It is imported by `AppModule` and the `GraphQLConfigModule`.

Key domain groupings registered here:

### Auth & Identity

- **`AuthModule`** — central authentication module: JWT strategy, SAML strategy, sign-in/up flows, OAuth controllers, SSO connection services, token issuance
- **`WorkspaceSSOModule`** — SAML/OIDC SSO resolver and service (`SSOService`, `SSOResolver`)
- **`AppTokenModule`**, **`ApiKeyModule`** — application-level token management
- **`ImpersonationModule`** — admin impersonation support
- **`TwoFactorAuthenticationModule`** — TOTP/2FA (declared inside `AuthModule`)

Sources: [packages/twenty-server/src/engine/core-modules/core-engine.module.ts:21-58]()

### Billing (Enterprise)

- **`BillingModule`** — Stripe-backed subscription management. Provides `BillingResolver`, `BillingSubscriptionService`, `BillingPortalWorkspaceService`, entitlement checks, credit rollover, usage caps, and workspace-member listeners. Entity models: `BillingSubscriptionEntity`, `BillingCustomerEntity`, `BillingEntitlementEntity`, `BillingProductEntity`, `BillingPriceEntity`, `BillingMeterEntity`.
- **`BillingWebhookModule`** — handles inbound Stripe webhook events
- **`AppBillingModule`** — billing for marketplace applications
- **`AiBillingModule`** — billing for AI model usage
- **`BillingGraphqlApiExceptionFilter`** — registered as `APP_FILTER` to translate billing exceptions into structured GraphQL errors

Sources: [packages/twenty-server/src/engine/core-modules/billing/billing.module.ts:45-90](), [packages/twenty-server/src/engine/core-modules/core-engine.module.ts:170-175]()

### Messaging & Calendar

- **`TimelineMessagingModule`** — exposes messaging timeline queries for the GraphQL API
- **`MessagingWebhooksModule`** — inbound webhooks for email providers
- **`TimelineCalendarEventModule`** — calendar event timeline queries
- **`ImapSmtpCaldavModule`** — IMAP/SMTP/CalDAV connection management
- **`ChannelSyncModule`**, **`SendEmailModule`** — channel sync state and outbound email dispatch (imported from `src/modules/`)

Sources: [packages/twenty-server/src/engine/core-modules/core-engine.module.ts:49-50, 75-77, 129-131]()

### Workflow

`WorkflowApiModule` (`packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts`) is the GraphQL-facing workflow surface. It declares:

- **Resolvers**: `WorkflowTriggerResolver`, `WorkflowBuilderResolver`, `WorkflowVersionStepResolver`, `WorkflowVersionEdgeResolver`, `WorkflowVersionResolver`
- **Controller**: `WorkflowTriggerController` (REST webhook entry point for external triggers)
- **Imports**: `WorkflowTriggerModule`, `WorkflowBuilderModule`, `WorkflowRunnerModule`, `WorkflowRunModule`, `WorkflowCommonModule`, `WorkflowVersionModule` (from `src/modules/workflow/`)

Sources: [packages/twenty-server/src/engine/core-modules/workflow/workflow-api.module.ts]()

### Workspace

- **`WorkspaceModule`** — workspace CRUD, activation lifecycle, provisioning
- **`WorkspaceInvitationModule`** — invitation flow
- **`UserModule`** — user entity, profile, preferences
- **`UserWorkspaceModule`** — user-workspace membership
- **`AdminPanelModule`** — admin-restricted operations

### Infrastructure Modules

| Module | Role |
|---|---|
| `FeatureFlagModule` | Runtime feature flag evaluation |
| `CacheStorageModule` | Redis-backed cache abstraction |
| `RedisClientModule` | Shared Redis client |
| `MessageQueueModule` | BullMQ job queue (registered async with factory) |
| `FileStorageModule` | S3 / local file storage abstraction |
| `EmailModule` | Transactional email dispatch |
| `MetricsModule` | Prometheus-style metrics |
| `ExceptionHandlerModule` | Sentry integration and error normalization |
| `LoggerModule` | Structured logging |
| `SearchModule` | Full-text search |
| `TelemetryModule` | Usage telemetry |
| `EventEmitterModule` (NestJS) | In-process event bus with wildcard support |
| `TwentyConfigModule` | Type-safe environment configuration |

Dynamic modules follow the `forRootAsync` pattern with factory functions and explicit dependency injection:

```typescript
// packages/twenty-server/src/engine/core-modules/core-engine.module.ts:133-148
MessageQueueModule.registerAsync({
  useFactory: messageQueueModuleFactory,
  inject: [TwentyConfigService, RedisClientService, MetricsService],
}),
ExceptionHandlerModule.forRootAsync({
  useFactory: exceptionHandlerModuleFactory,
  inject: [TwentyConfigService, HttpAdapterHost],
}),
```

Sources: [packages/twenty-server/src/engine/core-modules/core-engine.module.ts:83-169]()

---

## Auth Module Deep Dive

`AuthModule` is one of the most complex modules. Its structure:

### JWT Strategy

`JwtAuthStrategy` (`packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts`) extends `PassportStrategy(Strategy, 'jwt')`. Key behaviors:
- Uses a dynamic `SecretOrKeyProvider` rather than a static secret, delegating key resolution to `JwtWrapperService.resolveVerificationKey()` — this allows multiple signing keys and algorithm negotiation.
- Validates the resolved token against `JWT_SUPPORTED_VERIFY_ALGORITHMS`.
- Resolves workspace and user context from the validated payload, checking workspace activation status and permission flags.

### SSO / SAML Strategy

`SamlAuthStrategy` and `AuthSsoService` handle SAML assertion validation. The `SSOAuthController` is the ACS endpoint. `WorkspaceSSOModule` (an enterprise-licensed module) owns `SSOService` and the `SSOResolver` for managing identity providers via GraphQL.

### OAuth Controllers

`AuthModule` declares five REST controllers:

| Controller | Purpose |
|---|---|
| `GoogleAuthController` | Google OAuth sign-in callback |
| `MicrosoftAuthController` | Microsoft OAuth sign-in callback |
| `GoogleAPIsAuthController` | Google API scope grant callback |
| `MicrosoftAPIsAuthController` | Microsoft API scope grant callback |
| `OAuthPropagatorController` | Propagates OAuth tokens to workspaces |
| `SSOAuthController` | SAML SSO assertion consumer service |
| `ConnectionProviderOAuthController` | Marketplace app OAuth callbacks |

Sources: [packages/twenty-server/src/engine/core-modules/auth/auth.module.ts:133-141]()

### Token Services

`TokenModule` (imported by `AuthModule`) provides:
- `AccessTokenService` — short-lived JWT for API access
- `RefreshTokenService` — long-lived refresh flow
- `LoginTokenService` — one-time login link tokens
- `TransientTokenService` — temporary cross-step tokens used in OAuth flows

---

## Subscriptions Module

`SubscriptionsModule` (`packages/twenty-server/src/engine/subscriptions/subscriptions.module.ts`) is declared `@Global()` and provides real-time push infrastructure:

- **`EventStreamResolver`** — GraphQL subscription resolver (`Subscription` decorator)
- **`EventStreamService`** — manages active subscription channels
- **`SubscriptionService`** — coordinates subscription lifecycle
- **`ObjectRecordEventPublisher`** — publishes CRUD events for workspace objects into the event stream
- **`MetadataEventPublisher`** / **`MetadataEventEmitter`** — publishes metadata change events (object/field schema changes)
- **`MetadataEventsToDbListener`** — persists metadata events to the database
- **`WorkspaceEventBroadcaster`** — fans out workspace events to connected subscribers

Backing store is Redis via `CacheStorageModule`. The module imports `WorkspaceCacheModule` for workspace schema lookups and `WorkspaceManyOrAllFlatEntityMapsCacheModule` for flat entity resolution during event fan-out.

Sources: [packages/twenty-server/src/engine/subscriptions/subscriptions.module.ts]()

---

## Dataloaders

`DataloaderModule` / `DataloaderService` (`packages/twenty-server/src/engine/dataloaders/`) implement the [DataLoader pattern](https://github.com/graphql/dataloader) for N+1 query prevention in the GraphQL resolvers.

`DataloaderService.createLoaders()` is called once per request from the Yoga context factory. It returns an `IDataloaders` object containing named `DataLoader<K, V>` instances for:

- Field metadata resolution by workspace
- Object metadata resolution by workspace
- Relation metadata (including morph/polymorphic relations)
- View fields, filters, sorts, groups, field groups, filter groups
- Index metadata

The loaders use `WorkspaceManyOrAllFlatEntityMapsCacheModule` as the backing cache layer — they batch IDs across a single tick, look up from the flat entity cache, and return resolved DTOs (e.g. `FieldMetadataDTO`, `ObjectMetadataDTO`, `RelationDTO`).

Sources: [packages/twenty-server/src/engine/dataloaders/dataloader.module.ts](), [packages/twenty-server/src/engine/dataloaders/dataloader.service.ts:1-60]()

---

## API Surface Summary

```text
HTTP Routes
├── POST /graphql        — Core GraphQL Yoga (workspace data)
├── POST /metadata       — Metadata GraphQL Yoga (schema definitions)
├── POST /admin-panel    — Admin GraphQL Yoga
├── /rest/*path          — REST API (TwentyORM-backed)
└── /mcp                 — Model Context Protocol endpoint

GraphQL Schema Scopes
├── core                 → CoreEngineModule resolvers (code-first, autoSchemaFile)
├── metadata             → MetadataEngineModule resolvers
└── admin-panel          → AdminPanelGraphQLApiModule resolvers
```

All three GraphQL endpoints share the same `YogaDriver`. The core endpoint's `resolverSchemaScope: 'core'` and `include: [CoreEngineModule]` ensures only domain resolvers declared inside `CoreEngineModule` appear in the generated schema.

---

## Summary

Twenty's server architecture uses a clear two-tier NestJS module pattern: `AppModule` owns infrastructure bootstrapping (Yoga instances, ORM, middleware), while `CoreEngineModule` is a pure aggregator of ~65 domain modules covering auth (JWT + SAML + OAuth), billing (Stripe), messaging, calendar, workflow, workspace management, and SSO. The GraphQL Yoga gateway runs three independent endpoint instances, each with its own schema scope and plugin pipeline. Query complexity limits, introspection guards, Sentry tracing, and per-request DataLoaders are all wired through the central `GraphQLConfigService`. Real-time subscriptions are served via a global `SubscriptionsModule` backed by Redis pub/sub.

---

## 04. Workspace Runtime, Jobs & Instance Commands

> BullMQ background worker, queue-worker bootstrapping, database migration commands (RegisteredInstanceCommand / RegisteredWorkspaceCommand), upgrade pattern (fast vs slow instance commands), and the workspace event emitter that drives real-time side effects.

- Page Markdown: https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/04-workspace-runtime-jobs-instance-commands.md
- Generated: 2026-05-27T00:39:51.427Z

### Source Files

- `packages/twenty-server/src/queue-worker`
- `packages/twenty-server/src/engine/workspace-event-emitter`
- `packages/twenty-server/src/command`
- `packages/twenty-server/src/database`
- `packages/twenty-server/docs/UPGRADE_COMMANDS.md`
- `packages/twenty-server/src/modules/modules.module.ts`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/twenty-server/src/queue-worker/queue-worker.ts](packages/twenty-server/src/queue-worker/queue-worker.ts)
- [packages/twenty-server/src/queue-worker/queue-worker.module.ts](packages/twenty-server/src/queue-worker/queue-worker.module.ts)
- [packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts](packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts)
- [packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module.ts](packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module.ts)
- [packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts](packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts)
- [packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.module.ts](packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.module.ts)
- [packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts](packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts)
- [packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator.ts](packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator.ts)
- [packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts](packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts)
- [packages/twenty-server/src/database/commands/command-runners/workspace.command-runner.ts](packages/twenty-server/src/database/commands/command-runners/workspace.command-runner.ts)
- [packages/twenty-server/src/command/command.ts](packages/twenty-server/src/command/command.ts)
- [packages/twenty-server/docs/UPGRADE_COMMANDS.md](packages/twenty-server/docs/UPGRADE_COMMANDS.md)
</details>

# Workspace Runtime, Jobs & Instance Commands

Twenty's backend runs as two distinct processes: the main API server and a dedicated **queue worker**. The queue worker processes background jobs asynchronously using BullMQ and Redis, decoupling long-running tasks from request handling. Database schema evolution is handled by a separate command subsystem — **instance commands** and **workspace commands** — that replaces raw TypeORM migrations with a versioned, decorator-driven upgrade pipeline. A global `WorkspaceEventEmitter` bridges ORM mutations to downstream side effects such as webhooks, automation triggers, and audit records.

This page documents how the queue worker bootstraps, how the BullMQ driver manages queues and workers, how instance and workspace commands are declared and ordered, and how the workspace event emitter propagates database change events.

---

## Queue Worker Process

### Bootstrapping

The worker process is a standalone Node.js entry point that creates a headless NestJS application context — no HTTP server is started.

```ts
// packages/twenty-server/src/queue-worker/queue-worker.ts
async function bootstrap() {
  const app = await NestFactory.createApplicationContext(QueueWorkerModule, {
    bufferLogs: process.env.LOGGER_IS_BUFFER_ENABLED === 'true',
  });
  loggerService = app.get(LoggerService);
  app.useLogger(loggerService ?? false);
}
```

Sources: [packages/twenty-server/src/queue-worker/queue-worker.ts:9-33](packages/twenty-server/src/queue-worker/queue-worker.ts)

The root `QueueWorkerModule` wires the minimum set of modules required for job processing:

```ts
// packages/twenty-server/src/queue-worker/queue-worker.module.ts
@Module({
  imports: [
    CoreEngineModule,
    MessageQueueModule.registerExplorer(),  // registers BullMQ workers via discovery
    WorkspaceEventEmitterModule,
    JobsModule,
    TwentyORMModule,
    GlobalWorkspaceDataSourceModule,
  ],
})
export class QueueWorkerModule {}
```

Sources: [packages/twenty-server/src/queue-worker/queue-worker.module.ts:1-19](packages/twenty-server/src/queue-worker/queue-worker.module.ts)

`MessageQueueModule.registerExplorer()` activates the `MessageQueueExplorer` and `MessageQueueMetadataAccessor` via NestJS `DiscoveryModule`, which discovers all `@Processor`-decorated classes at startup and registers them as BullMQ workers.

Sources: [packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module.ts:22-27](packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module.ts)

### BullMQ Driver

The `BullMQDriver` class wraps the BullMQ `Queue` and `Worker` primitives. It maintains internal maps keyed by queue name (`MessageQueue` enum values):

```ts
private queueMap: Record<MessageQueue, Queue> = {} as Record<MessageQueue, Queue>;
private workerMap: Record<MessageQueue, Worker> = {} as Record<MessageQueue, Worker>;
```

**Queue registration** creates a `Queue` instance for each logical queue:

```ts
register(queueName: MessageQueue): void {
  this.queueMap[queueName] = new Queue(queueName, this.options);
}
```

**Worker registration** creates a BullMQ `Worker` that processes jobs with Sentry isolation scoping, structured logging, and execution time measurement:

```ts
work<T>(queueName, handler, options?) {
  this.workerMap[queueName] = new Worker(queueName, async (job) =>
    Sentry.withIsolationScope(async () => {
      await handler({ data: job.data, id: job.id ?? '', name: job.name });
    }),
    workerOptions,
  );
}
```

Sources: [packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts:81-161](packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts)

**Module lifecycle:** `onModuleInit` registers an OpenTelemetry observable gauge that reports the total number of waiting jobs across all queues. `onModuleDestroy` closes all queues and workers gracefully.

Sources: [packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts:56-93](packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts)

### Job Addition

The driver's `add` method accepts a job ID deduplication hint. When `options.id` is set, it scans the waiting list to prevent enqueueing a duplicate. The actual BullMQ job ID appends a UUID to allow a second waiting job once the first is being processed.

```ts
// Ensures only one waiting job exists for a given option.id
if (options?.id) {
  const waitingJobs = await this.queueMap[queueName].getJobs(['waiting']);
  const isJobAlreadyWaiting = waitingJobs.some(
    (job) => job.id?.slice(0, -(V4_LENGTH + 1)) === options.id,
  );
  if (isJobAlreadyWaiting) return;
}
```

Sources: [packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts:220-260](packages/twenty-server/src/engine/core-modules/message-queue/drivers/bullmq.driver.ts)

### Retention Policy

Completed and failed jobs are automatically pruned. Retention is controlled by constants in `queue-retention.constants.ts` (age and count limits), applied uniformly to every `add` and `addCron` call.

---

## Instance and Workspace Upgrade Commands

Twenty's upgrade pipeline replaces raw TypeORM migrations with a structured, decorator-based system that distinguishes between instance-level schema changes and per-workspace data migrations.

### Command Types

| Type | Interface | Decorator | Scope | When It Runs |
|---|---|---|---|---|
| Fast instance command | `FastInstanceCommand` | `@RegisteredInstanceCommand(version, ts)` | Core DB schema | Immediately on upgrade |
| Slow instance command | `SlowInstanceCommand` | `@RegisteredInstanceCommand(version, ts, { type: 'slow' })` | Core DB schema + data | With `--include-slow` flag |
| Workspace command | `WorkspaceCommandRunner` | `@RegisteredWorkspaceCommand(version, ts)` | Per-workspace schemas | After instance commands |

### Instance Commands

Instance commands operate on the shared `core` PostgreSQL schema and replace individual TypeORM migration files.

**Fast commands** run immediately during upgrade and implement `up` and `down` methods via a `QueryRunner`:

```ts
@RegisteredInstanceCommand('1.22.0', 1775758621017)
export class AddWorkspaceIdToTotoFastInstanceCommand implements FastInstanceCommand {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "core"."toto" ADD "workspaceId" uuid`);
  }
  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`ALTER TABLE "core"."toto" DROP COLUMN "workspaceId"`);
  }
}
```

**Slow commands** extend the fast interface by adding a `runDataMigration(dataSource)` step that executes before `up`. This handles bulk backfills that could be long-running:

```ts
@RegisteredInstanceCommand('1.22.0', 1775758621018, { type: 'slow' })
export class BackfillWorkspaceIdSlowInstanceCommand implements SlowInstanceCommand {
  async runDataMigration(dataSource: DataSource): Promise<void> { /* backfill rows */ }
  public async up(queryRunner: QueryRunner): Promise<void> { /* set NOT NULL */ }
  public async down(queryRunner: QueryRunner): Promise<void> { /* drop NOT NULL */ }
}
```

Sources: [packages/twenty-server/docs/UPGRADE_COMMANDS.md:22-74](packages/twenty-server/docs/UPGRADE_COMMANDS.md)

A common pairing pattern: a **fast** command adds a nullable column; a **slow** command backfills rows and then enforces `NOT NULL`.

### The `@RegisteredInstanceCommand` Decorator

The decorator is a function returning a `ClassDecorator`. It applies `@Injectable()` and stores version, timestamp, and type metadata via `Reflect.defineMetadata`:

```ts
export const RegisteredInstanceCommand =
  (version: TwentyAllVersion, timestamp: number, options?: { type: 'slow' }): ClassDecorator =>
  (target) => {
    Injectable()(target);
    Reflect.defineMetadata(
      REGISTERED_INSTANCE_COMMAND_KEY,
      { version, timestamp, type: options?.type ?? 'fast' },
      target,
    );
  };
```

Sources: [packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts:21-34](packages/twenty-server/src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator.ts)

### Workspace Commands

Workspace commands iterate over all active or suspended workspaces and apply per-workspace mutations. They combine the `@RegisteredWorkspaceCommand` metadata decorator with nest-commander's `@Command` decorator and extend `ActiveOrSuspendedWorkspaceCommandRunner`:

```ts
@RegisteredWorkspaceCommand('1.22.0', 1780000002000)
@Command({ name: 'upgrade:1-22:backfill-standard-skills', ... })
export class BackfillStandardSkillsCommand extends ActiveOrSuspendedWorkspaceCommandRunner {
  override async runOnWorkspace({ workspaceId, options }: RunOnWorkspaceArgs): Promise<void> {
    // per-workspace logic; options.dryRun and options.verbose are available
  }
}
```

Sources: [packages/twenty-server/docs/UPGRADE_COMMANDS.md:78-117](packages/twenty-server/docs/UPGRADE_COMMANDS.md)

The `WorkspaceCommandRunner` base class iterates workspaces through `WorkspaceIteratorService.iterate()`, exposing standard CLI flags automatically:

| Flag | Description |
|---|---|
| `-d, --dry-run` | Simulate without persisting changes |
| `-v, --verbose` | Enable verbose logging |
| `--workspace-id` | Limit execution to specified workspace IDs (repeatable) |
| `--start-from-workspace-id` | Resume from a specific workspace (ascending order) |
| `--workspace-count-limit` | Cap total workspaces processed |

Sources: [packages/twenty-server/src/database/commands/command-runners/workspace.command-runner.ts:41-137](packages/twenty-server/src/database/commands/command-runners/workspace.command-runner.ts)

### Command Registry and Execution Order

The `UpgradeCommandRegistryService` collects all decorated commands via their metadata, groups them by version into `VersionBundle` objects, and validates that no timestamp collision exists within the same kind:

```ts
type VersionBundle = {
  fastInstanceCommands: RegisteredFastInstanceCommand[];
  slowInstanceCommands: RegisteredSlowInstanceCommand[];
  workspaceCommands: RegisteredWorkspaceCommand[];
};
```

Within each version, the upgrade pipeline runs commands in this strict order, sorted by timestamp within each group:

1. **Fast instance commands** — immediate schema changes
2. **Slow instance commands** — data migrations + schema enforcement (requires `--include-slow`)
3. **Workspace commands** — per-workspace changes, applied sequentially

Sources: [packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts:24-50](packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-command-registry.service.ts), [packages/twenty-server/docs/UPGRADE_COMMANDS.md:109-118](packages/twenty-server/docs/UPGRADE_COMMANDS.md)

### Generating Instance Commands

A dedicated CLI command scaffolds the boilerplate file and registers it automatically:

```bash
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
```

This writes a timestamped file and updates `instance-commands.constant.ts`. **Do not edit that constant file manually.**

---

## Workspace Event Emitter

The `WorkspaceEventEmitter` is a global NestJS service that translates ORM-level mutations into typed event batches, propagated through `EventEmitter2`.

```ts
@Global()
@Module({
  providers: [WorkspaceEventEmitter],
  exports: [WorkspaceEventEmitter],
})
export class WorkspaceEventEmitterModule {}
```

Sources: [packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.module.ts:1-10](packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.module.ts)

### API

**`emitDatabaseBatchEvent`** — emits a typed batch of record lifecycle events (created, updated, deleted, destroyed, restored, upserted). The event name is computed from the object metadata name and action:

```ts
public emitDatabaseBatchEvent<T, A extends keyof ActionEventMap<T>>(
  databaseBatchEventInput: DatabaseBatchEventInput<T, A> | undefined,
) {
  const eventName = computeEventName(objectMetadataNameSingular, action);
  const workspaceEventBatch: WorkspaceEventBatch<ActionEventMap<T>[A]> = {
    name: eventName, workspaceId, objectMetadata, events,
  };
  this.eventEmitter.emit(eventName, workspaceEventBatch);
}
```

**`emitCustomBatchEvent`** — emits an arbitrary named batch event useful for domain-level signals (e.g., email received, calendar sync complete) that do not correspond to a direct ORM entity mutation.

Sources: [packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts:42-89](packages/twenty-server/src/engine/workspace-event-emitter/workspace-event-emitter.ts)

### Integration with TwentyORM

The workspace entity manager holds a reference to `WorkspaceEventEmitter` through the connection context and calls `emitDatabaseBatchEvent` at the end of mutating operations (save, remove, soft-remove, etc.), ensuring that downstream subscribers — webhooks, automation jobs, audit logs — are notified after every persisted change.

Sources: [packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts:86-91](packages/twenty-server/src/engine/twenty-orm/entity-manager/workspace-entity-manager.ts)

### Event Name Pattern

```
<objectNameSingular>.<action>
// e.g.:  person.created  |  company.updated  |  note.deleted
```

Event subscribers declared with NestJS `@OnEvent('person.created')` (or a wildcard `*.created`) receive a `WorkspaceEventBatch` containing the `workspaceId`, object metadata, and the list of record event payloads.

---

## Runtime Architecture Overview

```text
┌──────────────────────────────────────┐  ┌────────────────────────────────────────┐
│         API Server Process           │  │       Queue Worker Process             │
│  (NestJS HTTP + GraphQL)             │  │  (NestJS ApplicationContext, no HTTP)  │
│                                      │  │                                        │
│  MessageQueueService.add() ──────────┼──► BullMQDriver.add()                    │
│  WorkspaceEventEmitter               │  │  BullMQDriver.work()                   │
│                                      │  │    └─ Worker per MessageQueue enum     │
│  CommandModule (nest-commander)      │  │  JobsModule (all job handlers)         │
└──────────────────────────────────────┘  └────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│       Upgrade Command Pipeline  (npx nx run ...:database:...)    │
│                                                                  │
│  CommandModule (nest-commander)                                  │
│    └─ UpgradeCommandRegistryService                             │
│         ├─ Fast instance commands  (immediate schema DDL)        │
│         ├─ Slow instance commands  (data migration + DDL)        │
│         └─ Workspace commands      (per-workspace iteration)     │
└──────────────────────────────────────────────────────────────────┘
```

---

## Summary

Twenty's worker runtime separates concerns cleanly: the queue-worker process is a minimal NestJS context that activates BullMQ workers via module discovery, with no HTTP surface. Instance commands provide a versioned, rollback-aware alternative to raw TypeORM migrations, split between fast DDL-only changes and slow data-migration-plus-DDL pairs. Workspace commands apply per-workspace mutations across all active workspaces in a controlled, optionally resumable iteration. The global `WorkspaceEventEmitter` ties all ORM mutations to downstream event subscribers through typed batch events, enabling webhooks, automation, and observability without tight coupling to the data layer.

---

## 05. Frontend Module Map & State Architecture

> React 18 / Jotai / Apollo Client architecture: how modules (object-record, settings, auth, workflow, command-menu, navigation) are structured, how global state atoms compose with GraphQL cache, and the Linaria/Lingui conventions. Covers the twenty-ui shared component library and the metadata-store that mirrors backend schema on the client.

- Page Markdown: https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/05-frontend-module-map-state-architecture.md
- Generated: 2026-05-27T05:00:53.610Z

### Source Files

- `packages/twenty-front/src/modules/object-record`
- `packages/twenty-front/src/modules/settings`
- `packages/twenty-front/src/modules/auth`
- `packages/twenty-front/src/modules/metadata-store`
- `packages/twenty-front/src/modules/workflow`
- `packages/twenty-front/src/modules/command-menu`
- `packages/twenty-front/src/modules/navigation`
- `packages/twenty-ui/src`

> ⚠️ The agent returned an invalid wiki page. This page needs recovery.
>
> First failure: the page did not include the required "# Frontend Module Map & State Architecture" heading near the top
> Retry failure: the page did not include the required "# Frontend Module Map & State Architecture" heading near the top

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts](packages/twenty-front/src/modules/metadata-store/states/metadataStoreState.ts)
- [packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts](packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts)
- [packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsWithFieldsSelector.ts](packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsWithFieldsSelector.ts)
- [packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts](packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts)
- [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts](packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomState.ts)
- [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomComponentFamilyState.ts](packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomComponentFamilyState.ts)
- [packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap.ts](packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap.ts)
- [packages/twenty-front/src/modules/auth/states/currentWorkspaceMemberState.ts](packages/twenty-front/src/modules/auth/states/currentWorkspaceMemberState.ts)
- [packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts](packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts)
- [packages/twenty-front/src/modules/command-menu-item/contexts/CommandMenuContextProviderContent.tsx](packages/twenty-front/src/modules/command-menu-item/contexts/CommandMenuContextProviderContent.tsx)
- [packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useNavigationSection.ts](packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useNavigationSection.ts)
- [packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx](packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx)
- [packages/twenty-front/src/modules/workflow/types/Workflow.ts](packages/twenty-front/src/modules/workflow/types/Workflow.ts)
- [packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts](packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts)
</details>

# Frontend Module Map & State Architecture

The Twenty frontend (`packages/twenty-front`) is a single-page React 18 application designed for maximum extensibility and performance. It organizes core domain logic into distinct modules under `src/modules/`. This design encapsulates files related to specific features—including components, custom hooks, state atoms, types, and GraphQL assets—preventing cross-domain spaghetti code.

The state management architecture relies on a hybrid model that separates concern: **Apollo Client** acts as the authoritative engine for normalized remote database record caches, while **Jotai** handles local UI state, authentication details, and database schema representations. By utilizing Jotai custom wrappers, Twenty maintains client-side persistence and per-instance state isolation. This wiki outlines the module mapping, state architecture, and UI paradigms of the Twenty frontend.

---

## State & Data Flow Architecture

```mermaid
flowchart TB
    %% Subgraphs for visual layers
    subgraph UI ["twenty-front UI & Styling (twenty-ui)"]
        Components["React 18 Components"]
        LinariaStyled["Linaria zero-runtime CSS-in-JS<br/>(using themeCssVariables)"]
        LinguiMacro["Lingui i18n Macros<br/>(Trans, t template tag)"]
        Components --> LinariaStyled
        Components --> LinguiMacro
    end

    subgraph State ["Client-Side State Management (Jotai)"]
        subgraph Atoms ["Atom Types & Collections"]
            StateAtom["State (createAtomState)<br/>- local/session storage / cookies"]
            FamilyState["FamilyState (createAtomFamilyState)<br/>- e.g., metadataStoreState"]
            CompFamilyState["ComponentFamilyState (createAtomComponentFamilyState)<br/>- per-instance context isolation"]
        end
        Selectors["Derived Selectors (createAtomSelector)<br/>- e.g., objectMetadataItemsWithFieldsSelector"]
        Atoms --> Selectors
    end

    subgraph ServerData ["Server Data & Schema Sync (Apollo Client)"]
        ApolloFactory["ApolloFactory (Apollo Client Manager)"]
        MetadataStore["Metadata Store Module<br/>(Schema mirror + hash checking)"]
        ApolloFactory -->|FIND_MINIMAL_METADATA| MetadataStore
        MetadataStore -->|Writes current / draft| FamilyState
    end

    subgraph RecordCRUD ["Dynamic Record Operations (object-record)"]
        RecordHooks["Record Hooks (e.g., useAggregateRecords)"]
        GqlQueryBuilder["GQL Dynamic Document Builder<br/>(e.g., useAggregateRecordsQuery)"]
        
        Selectors -->|Reads Enriched Schema| RecordHooks
        RecordHooks --> GqlQueryBuilder
        GqlQueryBuilder -->|Executes Dynamic Query| ApolloFactory
    end

    %% Dependencies between layers
    Components -->|Reads State/Selectors| Selectors
    Components -->|Calls Record Hooks| RecordHooks
    ApolloFactory -->|Normalized Cache / CRUD| RecordHooks
```

---

## Module Directory Map

The core domain logic of `twenty-front` is organized as decoupled modules. Each module owns its specific features:

| Module | Core Responsibility | Key Files / Subdirectories |
| :--- | :--- | :--- |
| **`auth`** | Session management, OAuth logins, cookies, bypass mechanisms, and auth tokens. | `states/tokenPairState.ts`<br/>`sign-in-up/` |
| **`metadata-store`** | Mirrors the backend schema locally to support dynamic UI layout generation. | `states/metadataStoreState.ts`<br/>`hooks/useLoadMinimalMetadata.ts` |
| **`object-metadata`** | Resolves dynamic object configurations and calculates access control/permissions. | `states/objectMetadataItemsWithFieldsSelector.ts`<br/>`hooks/useObjectMetadataItem.ts` |
| **`object-record`** | Dynamic record operations, filters, sorting, offsets, and paginated aggregates. | `hooks/useAggregateRecords.ts`<br/>`hooks/useUpdateOneRecord.ts` |
| **`command-menu`** | The main navigation command menu launcher, action registrations, and filters. | `states/contexts/CommandMenuComponentInstanceContext.ts` |
| **`command-menu-item`**| Context-aware rendering of specific command items depending on the view. | `contexts/CommandMenuContextProviderContent.tsx` |
| **`navigation`** | Control drawer layout status, history stacks, and contextual breadcrumbs. | `states/lastVisitedViewPerObjectMetadataItemState.ts` |
| **`workflow`** | UI builder for automation steps and trigger event configurations. | `types/Workflow.ts` |
| **`ui`** | Generic helper modules, Jotai wrapper creators, and component instances context managers. | `utilities/state/jotai/utils/createAtomState.ts` |

---

## Jotai Custom State Wrappers & Isolation

To simplify state definitions and add persistent properties, Twenty wraps Jotai in custom state builders:
1. **`State` (`createAtomState`)**: Standard Jotai atom configured to support optional memory-only storage, session storage, cookies, or `localStorage` backups.
2. **`FamilyState` (`createAtomFamilyState`)**: Creates a key-value family lookup map of atoms (used extensively for metadata items).
3. **`ComponentFamilyState` (`createAtomComponentFamilyState`)**: Maps components to a specific instance ID so that multiple component instances (e.g., side panels, search drawers, table aggregations) do not share or override each other's state values.

### Scoping State via Component Instances
To achieve isolation, Twenty uses a global context mapping model. An `instanceId` is passed down using a standard React context created with `createComponentInstanceContext`. The component registers its status in a global namespace.

```typescript
// packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap.ts
class ComponentInstanceContextMap {
  constructor() {
    if (!isDefined(window.componentComponentStateContextMap)) {
      window.componentComponentStateContextMap = new Map();
    }
  }

  public get(key: string): ComponentInstanceStateContext<any> | undefined {
    return window.componentComponentStateContextMap.get(key);
  }

  public set(key: string, context: ComponentInstanceStateContext<any>) {
    window.componentComponentStateContextMap.set(key, context);
  }
}
```
*Sources: [packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/globalComponentInstanceContextMap.ts:4-22]()*

Component family states use this map to cache and query scoped instances:
```typescript
// packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomComponentFamilyState.ts
export const createAtomComponentFamilyState = <ValueType, FamilyKey>({
  key,
  defaultValue,
  componentInstanceContext,
}: {
  key: string;
  defaultValue: ValueType;
  componentInstanceContext: ComponentInstanceStateContext<any> | null;
}) => {
  if (isDefined(componentInstanceContext)) {
    globalComponentInstanceContextMap.set(key, componentInstanceContext);
  }

  const atomCache = new Map<string, any>();

  const familyFunction = ({ instanceId, familyKey }) => {
    const familyKeyStr = typeof familyKey === 'string' ? familyKey : JSON.stringify(familyKey);
    const cacheKey = `${instanceId}__${familyKeyStr}`;
    const existing = atomCache.get(cacheKey);

    if (existing !== undefined) return existing;

    const baseAtom = atom(defaultValue);
    baseAtom.debugLabel = `${key}__${cacheKey}`;
    atomCache.set(cacheKey, baseAtom);
    return baseAtom;
  };

  return { type: 'ComponentFamilyState', key, atomFamily: familyFunction };
};
```
*Sources: [packages/twenty-front/src/modules/ui/utilities/state/jotai/utils/createAtomComponentFamilyState.ts:11-56]()*

---

## Metadata Store Lifecycle & Schema Mirroring

Twenty uses a client-side "metadata store" to avoid querying structural databases continuously. This cache contains every custom object metadata, roles, indexes, and layout settings defined in the workspace.

### Schema Syncing
During initialization, the `useLoadMinimalMetadata` hook executes a fast `FIND_MINIMAL_METADATA` query containing an array of collection hashes:

```typescript
// packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts
const { objectMetadataItems, views, collectionHashes } = result.data.minimalMetadata;
const staleEntityKeys: MetadataEntityKey[] = [];

if (isDefined(collectionHashes)) {
  for (const { collectionName, hash } of collectionHashes) {
    const entityKey = mapAllMetadataNameToEntityKey(collectionName);
    if (!isDefined(entityKey)) continue;

    const entry = store.get(metadataStoreState.atomFamily(entityKey));
    if (entry.currentCollectionHash !== hash) {
      staleEntityKeys.push(entityKey);
    }

    store.set(metadataStoreState.atomFamily(entityKey), (prev) => ({
      ...prev,
      draftCollectionHash: hash,
    }));
  }
}
```
*Sources: [packages/twenty-front/src/modules/metadata-store/hooks/useLoadMinimalMetadata.ts:30-58]()*

The client compares these hashes with its local `currentCollectionHash`. If the hash doesn't match, the collection is designated as stale, prompting a lazy sync request for that specific entity type.

### Derived Permission Filtering
To guarantee that users do not interact with columns or objects they are not allowed to access, derived Jotai selectors filter this cached schema dynamically. For instance, the `objectMetadataItemsWithFieldsSelector` resolves current workspace permissions:

```typescript
// packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsWithFieldsSelector.ts
export const objectMetadataItemsWithFieldsSelector = createAtomSelector<EnrichedObjectMetadataItem[]>({
  key: 'objectMetadataItemsWithFieldsSelector',
  get: ({ get }) => {
    const flatObjects = get(flatObjectMetadataItemsSelector);
    const allFlatFields = get(fieldMetadataItemsSelector);
    const allFlatIndexes = get(indexMetadataItemsSelector);
    const currentUserWorkspace = get(currentUserWorkspaceState);

    // Grouping by object ID and applying workspace member permission settings
    return flatObjects.map((flatObject) => {
      const fields = fieldsByObjectId.get(flatObject.id) ?? [];
      const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
        objectPermissionsByObjectMetadataId,
        objectMetadataId: flatObject.id,
      });

      const nonReadableFieldMetadataIds = getNonReadableFieldMetadataIdsFromObjectPermissions({ objectPermissions });
      const nonUpdatableFieldMetadataIds = getNonUpdatableFieldMetadataIdsFromObjectPermissions({ objectPermissions });

      return {
        ...flatObject,
        fields,
        readableFields: fields.filter((field) => !nonReadableFieldMetadataIds.includes(field.id)),
        updatableFields: fields.filter((field) => !nonUpdatableFieldMetadataIds.includes(field.id)),
      };
    });
  }
});
```
*Sources: [packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsWithFieldsSelector.ts:13-93]()*

---

## Dynamic CRUD Generation & Apollo Strategy

Dynamic database actions (like viewing a custom CRM object table) are orchestrated dynamically on the fly by combining metadata selectors and custom Apollo queries.

1. The React hook (e.g., `useAggregateRecords`) queries the schema through `useObjectMetadataItem`.
2. It fetches the dynamic GraphQL query structure based on cached properties using helper builders like `useAggregateRecordsQuery`.
3. It validates user access configurations (e.g., `canReadObjectRecords`).
4. Finally, it triggers Apollo Client to fetch and store the response normalized cache inside the application.

```typescript
// packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts
export const useAggregateRecords = <T extends AggregateRecordsData>({
  objectNameSingular,
  filter,
  recordGqlFieldsAggregate,
  skip,
}) => {
  const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular });
  const apolloCoreClient = useApolloCoreClient();
  const { aggregateQuery, gqlFieldToFieldMap } = useAggregateRecordsQuery({
    objectNameSingular,
    recordGqlFieldsAggregate,
  });

  const objectPermissions = useObjectPermissionsForObject(objectMetadataItem.id);
  const hasReadPermission = objectPermissions.canReadObjectRecords;

  const { data, loading, error } = useQuery<RecordGqlOperationFindManyResult>(
    aggregateQuery,
    {
      skip: skip || !isDefined(objectMetadataItem) || !hasReadPermission,
      variables: { filter },
      client: apolloCoreClient,
    },
  );

  // Formats data map after completion...
  return { objectMetadataItem, data: formattedData, loading, error };
};
```
*Sources: [packages/twenty-front/src/modules/object-record/hooks/useAggregateRecords.ts:20-80]()*

---

## Command Menu Filter & Context Evaluation

The command bar renders dynamic actions (like opening settings, running logic functions, or trigger editing actions) contextually. Using `CommandMenuContextProviderContent`, the module evaluates custom JSON expressions to hide or show commands on the fly:

```typescript
// packages/twenty-front/src/modules/command-menu-item/contexts/CommandMenuContextProviderContent.tsx
const filteredCommandMenuItems = useMemo(() => {
  const currentObjectMetadataItemId = commandMenuContextApi.objectMetadataItem.id;
  const hasSelectedRecords = commandMenuContextApi.numberOfSelectedRecords > 0;
  
  const commandMenuItemsToDisplay = isInPreviewMode
    ? (commandMenuItemsDraft ?? commandMenuItems)
    : commandMenuItems;

  return commandMenuItemsToDisplay
    .filter(doesCommandMenuItemMatchObjectMetadataId(currentObjectMetadataItemId))
    .filter(doesCommandMenuItemMatchPageType(commandMenuContextApi.pageType))
    .filter(doesCommandMenuItemMatchSelectionState(hasSelectedRecords))
    .filter(doesCommandMenuItemMatchPageLayoutId(currentPageLayoutId))
    .filter((item) =>
      evaluateConditionalAvailabilityExpression(
        item.conditionalAvailabilityExpression,
        commandMenuContextApi,
      ),
    )
    .sort((firstItem, secondItem) => firstItem.position - secondItem.position);
}, [commandMenuContextApi, commandMenuItems, commandMenuItemsDraft, currentPageLayoutId, isInPreviewMode]);
```
*Sources: [packages/twenty-front/src/modules/command-menu-item/contexts/CommandMenuContextProviderContent.tsx:36-67]()*

---

## Styling (Linaria) & Translation (Lingui) Conventions

### Theme Constants via Zero-Runtime CSS Custom Properties
Twenty avoids utility-first styling classes like Tailwind CSS, adopting **Linaria** to create zero-runtime CSS-in-JS styled elements. Instead of inlining hex colors, styles reference `themeCssVariables` imported from `twenty-ui/theme-constants`. Under the hood, this compiles to standard CSS custom properties (`--font-color-tertiary`, etc.). Changing themes dynamically is done by reassigning root CSS classes (e.g. from `.theme-light` variables to `.theme-dark`).

### Dynamic Translation via Lingui Macros
To support localization (i18n), Twenty utilizes `@lingui/react/macro` tags (like `<Trans>`) rather than hardcoding static text nodes. This ensures all user-visible strings are automatically extractable for translations during the build pipeline.

```tsx
// packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx
import { styled } from '@linaria/react';
import { Trans } from '@lingui/react/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';

const StyledCopyContainer = styled.div`
  align-items: center;
  color: ${themeCssVariables.font.color.tertiary};
  font-size: ${themeCssVariables.font.size.sm};
  max-width: 280px;
  text-align: center;
`;

export const FooterNote = () => {
  return (
    <StyledCopyContainer>
      <Trans>By using Twenty, you agree to the</Trans>{' '}
      <a href="https://twenty.com/legal/terms" target="_blank" rel="noopener noreferrer">
        <Trans>Terms of Service</Trans>
      </a>
    </StyledCopyContainer>
  );
};
```
*Sources: [packages/twenty-front/src/modules/auth/sign-in-up/components/FooterNote.tsx:1-83]()*

---

## Summary

The Twenty frontend leverages React 18, Jotai, and Apollo Client to build a highly responsive, custom-schema-driven application. By caching the workspace database schema in a hash-checked local `metadataStoreState` atom, the client can resolve permissions, construct dynamic GraphQL requests, and customize layout views without blocking network round-trips. Furthermore, Twenty isolates parallel component states using custom `ComponentFamilyState` wrappers, designs performant zero-runtime styled boundaries through Linaria, and localizes all user interfaces via Lingui macros.

---

## 06. Integrations, Extension Points & Operational Surfaces

> External integration surfaces: Zapier connector, REST/GraphQL client SDK (twenty-client-sdk), email templates (twenty-emails), connected-account sync (messaging, calendar), AI/code-interpreter modules, and the Docker Compose dev/prod setup. How to extend Twenty with new workspace modules, custom objects, and external connectors.

- Page Markdown: https://grok-wiki.com/public/wiki/twentyhq-twenty-7ed82e5a21f6/pages/06-integrations-extension-points-operational-surfaces.md
- Generated: 2026-05-27T00:43:00.731Z

### Source Files

- `packages/twenty-zapier`
- `packages/twenty-client-sdk`
- `packages/twenty-emails/src`
- `packages/twenty-server/src/modules/connected-account`
- `packages/twenty-server/src/engine/core-modules/ai`
- `packages/twenty-docker`
- `packages/twenty-e2e-testing`
- `packages/twenty-utils/setup-dev-env.sh`

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [packages/twenty-zapier/src/index.ts](packages/twenty-zapier/src/index.ts)
- [packages/twenty-zapier/src/authentication.ts](packages/twenty-zapier/src/authentication.ts)
- [packages/twenty-zapier/src/triggers/trigger_record.ts](packages/twenty-zapier/src/triggers/trigger_record.ts)
- [packages/twenty-client-sdk/src/generate/twenty-client-template.ts](packages/twenty-client-sdk/src/generate/twenty-client-template.ts)
- [packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts](packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts)
- [packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts](packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts)
- [packages/twenty-server/src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job.ts](packages/twenty-server/src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job.ts)
- [packages/twenty-server/src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job.ts](packages/twenty-server/src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job.ts)
- [packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts](packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts)
- [packages/twenty-docker/docker-compose.yml](packages/twenty-docker/docker-compose.yml)
- [packages/twenty-docker/docker-compose.dev.yml](packages/twenty-docker/docker-compose.dev.yml)
</details>

# Integrations, Extension Points & Operational Surfaces

Twenty is structured as an Nx monorepo in which each major external integration surface lives in its own package or server module. This page documents how Twenty exposes itself to the outside world (Zapier, GraphQL/REST clients, email), how it pulls external data in (connected accounts for Gmail, Microsoft, and calendar), how background workers process integration events, and how the full system is operated via Docker Compose for both development and production.

Understanding these surfaces is essential for anyone extending Twenty with new workspace modules, custom objects, or external connectors, since each integration pattern follows distinct lifecycle, authentication, and worker-queue conventions.

---

## Zapier Connector (`packages/twenty-zapier`)

Twenty ships a first-party Zapier integration built with `zapier-platform-core`. It is published as its own package and connects to the Twenty GraphQL API using an API key.

### Authentication

The connector uses Zapier's `custom` authentication type. Users provide:

| Field | Required | Description |
|---|---|---|
| `apiKey` | Yes | API key created in Twenty workspace settings |
| `apiUrl` | No | Override URL for self-hosted instances |

Authentication is tested by executing a `currentWorkspace { id displayName }` metadata query. The connection label shown in Zapier is derived from `currentWorkspace.displayName`.

Sources: [packages/twenty-zapier/src/authentication.ts:5-39](packages/twenty-zapier/src/authentication.ts)

### Triggers and Actions

The connector registers three triggers and one action:

| Component | Key | Purpose |
|---|---|---|
| Trigger | `find_object_names_singular` | Enumerates workspace object types (used to populate dynamic dropdowns) |
| Trigger | `list_record_ids` | Lists record IDs for a given object type |
| Trigger | `trigger_record` | Webhook trigger — fires on `created`, `updated`, `deleted`, or `destroyed` events for any object |
| Action | `crud_record` | Creates or updates a record of a selected object type |

The `trigger_record` component uses Zapier's `hook` type (REST webhooks). It calls `performSubscribe` / `performUnsubscribe` to register and deregister Twenty webhooks, and `performList` to backfill recent records.

Sources: [packages/twenty-zapier/src/index.ts:1-27](packages/twenty-zapier/src/index.ts), [packages/twenty-zapier/src/triggers/trigger_record.ts:10-59](packages/twenty-zapier/src/triggers/trigger_record.ts)

---

## TypeScript Client SDK (`packages/twenty-client-sdk`)

Twenty generates a workspace-specific TypeScript client at runtime via a background job. The generated client wraps `genql` (a type-safe GraphQL code generator) and injects `TwentyGeneratedClient` as a thin layer on top.

### `TwentyGeneratedClient`

The core class in `twenty-client-template.ts` handles:

- **Authorization token resolution**: checks `Authorization` header → `TWENTY_APP_ACCESS_TOKEN` env var → legacy `TWENTY_API_KEY` env var, in that priority order.
- **Transparent token refresh**: on a 401-style error, `executeGraphqlRequestWithOptionalRefresh` retries the request once with a refreshed token using a shared `refreshAccessTokenPromise` to prevent duplicate refresh calls.
- **File upload**: `uploadFile()` encodes the multipart GraphQL upload mutation (`UploadFilesFieldFileByUniversalIdentifier`) using the `FormData` / `map` protocol.
- **Custom fetch**: callers can inject their own `fetch` implementation for environments without `globalThis.fetch`.

```typescript
// packages/twenty-client-sdk/src/generate/twenty-client-template.ts
const APP_ACCESS_TOKEN_ENV_KEY = 'TWENTY_APP_ACCESS_TOKEN';
const API_KEY_ENV_KEY = 'TWENTY_API_KEY';

export class TwentyGeneratedClient {
  query<R extends QueryGenqlSelection>(request: R) { ... }
  mutation<R extends MutationGenqlSelection>(request: R) { ... }
  async uploadFile(fileBuffer, filename, contentType, fieldMetadataUniversalIdentifier) { ... }
}
```

### SDK Generation Job

The server generates per-application clients asynchronously via `GenerateSdkClientJob`, which delegates to `SdkClientGenerationService`. The job is dispatched when an application is provisioned or its schema changes.

Sources: [packages/twenty-client-sdk/src/generate/twenty-client-template.ts:34-190](packages/twenty-client-sdk/src/generate/twenty-client-template.ts), [packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts:11-23](packages/twenty-server/src/engine/core-modules/sdk-client/jobs/generate-sdk-client.job.ts)

---

## Email Templates (`packages/twenty-emails`)

Transactional emails are React components rendered with [React Email](https://react.email). The `src/emails/` directory contains one component per email type:

| File | Purpose |
|---|---|
| `send-invite-link.email.tsx` | Workspace invitation |
| `send-email-verification-link.email.tsx` | Email address verification |
| `password-reset-link.email.tsx` | Password reset |
| `password-update-notify.email.tsx` | Password change notification |
| `warn-suspended-workspace.email.tsx` | Pre-suspension warning |
| `clean-suspended-workspace.email.tsx` | Post-suspension cleanup notice |
| `validate-approved-access-domain.email.tsx` | Domain approval confirmation |

The `EmailSenderJob` in the server picks up queued email tasks and renders + dispatches them via the configured SMTP driver (see Docker Compose environment variables).

---

## Connected Accounts: Messaging & Calendar Sync

Connected accounts are the mechanism by which Twenty links external provider credentials (Gmail, Microsoft, IMAP/SMTP/CalDAV) to workspace members. A `ConnectedAccountDTO` carries provider identity, tokens, and scopes.

```typescript
// packages/twenty-server/src/engine/metadata-modules/connected-account/dtos/connected-account.dto.ts
export class ConnectedAccountDTO {
  handle: string;           // email address
  provider: string;         // e.g. "google", "microsoft"
  accessToken: string | null;    // @HideField — never returned to clients
  refreshToken: string | null;   // @HideField
  scopes: string[] | null;
  connectionParameters: ImapSmtpCaldavParams | null;  // @HideField
}
```

### Messaging Sync

Incoming messages are imported via the `messagingQueue` (BullMQ). When a connected account is removed, `MessagingConnectedAccountDeletionCleanupJob` cleans orphaned messages and threads:

```typescript
// @Processor({ queueName: MessageQueue.messagingQueue, scope: Scope.REQUEST })
async handle(data): Promise<void> {
  await this.messageCleanerService.cleanOrphanMessagesAndThreads(data.workspaceId);
}
```

The workspace entity model (`MessageWorkspaceEntity`) stores subject, body text, received timestamp, and relations to threads, participants, and channel associations.

Sources: [packages/twenty-server/src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job.ts:1-30](packages/twenty-server/src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job.ts), [packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts:1-27](packages/twenty-server/src/modules/messaging/common/standard-objects/message.workspace-entity.ts)

### Calendar Sync

Calendar events are processed on the `calendarQueue`. Disconnecting an account dispatches `DeleteConnectedAccountAssociatedCalendarDataJob`, which calls `CalendarEventCleanerService.cleanWorkspaceCalendarEvents`:

```typescript
// @Processor(MessageQueue.calendarQueue)
async handle(data): Promise<void> {
  await this.calendarEventCleanerService.cleanWorkspaceCalendarEvents(data.workspaceId);
}
```

Sources: [packages/twenty-server/src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job.ts:1-25](packages/twenty-server/src/modules/calendar/calendar-event-cleaner/jobs/delete-connected-account-associated-calendar-data.job.ts)

### Provider Enablement

Providers are enabled via environment variables. All are commented out (disabled) by default in the Docker Compose files. Enable them by uncommenting and supplying credentials:

| Variable | Provider |
|---|---|
| `MESSAGING_PROVIDER_GMAIL_ENABLED` | Gmail sync |
| `CALENDAR_PROVIDER_GOOGLE_ENABLED` | Google Calendar |
| `MESSAGING_PROVIDER_MICROSOFT_ENABLED` | Microsoft mail |
| `CALENDAR_PROVIDER_MICROSOFT_ENABLED` | Microsoft calendar |
| `AUTH_GOOGLE_CLIENT_ID / SECRET` | Google OAuth |
| `AUTH_MICROSOFT_CLIENT_ID / SECRET` | Microsoft OAuth |

---

## Docker Compose: Dev and Production Deployments

Two Compose files ship in `packages/twenty-docker/`:

### `docker-compose.dev.yml` — Local Development Infrastructure

Starts Postgres 16 and Redis 7 only, with ports exposed to `localhost`. Application code runs locally from source. This is also what `packages/twenty-utils/setup-dev-env.sh` uses when the `--docker` flag is passed.

```yaml
services:
  db:    # postgres:16, port 5432
  redis: # redis:7, port 6379, --maxmemory-policy noeviction
```

Sources: [packages/twenty-docker/docker-compose.dev.yml:1-43](packages/twenty-docker/docker-compose.dev.yml)

### `docker-compose.yml` — Production / Full Stack

Runs the complete application stack from the `twentycrm/twenty:${TAG:-latest}` image:

```
┌─────────────────────────────────────────────────┐
│  server  (port 3000)  — NestJS + frontend        │
│  worker               — yarn worker:prod          │
│  db       postgres:16  — persistent volume        │
│  redis    redis        — noeviction               │
└─────────────────────────────────────────────────┘
```

Key design points:
- **server** and **worker** share the same image; the worker is distinguished solely by its command (`yarn worker:prod`).
- The worker sets `DISABLE_DB_MIGRATIONS=true` and `DISABLE_CRON_JOBS_REGISTRATION=true` — both are the server's responsibility.
- Storage can be local (volume `server-local-data`) or S3-compatible (`STORAGE_TYPE`, `STORAGE_S3_*` variables).
- Encryption keys (`ENCRYPTION_KEY`, `APP_SECRET`) are mandatory; messaging/calendar providers and SMTP are opt-in via commented variables.
- Health checks: server polls `/healthz`; Postgres uses `pg_isready`; Redis uses `redis-cli ping`.

Sources: [packages/twenty-docker/docker-compose.yml:1-138](packages/twenty-docker/docker-compose.yml)

---

## Extension Points

### Workspace Modules and Custom Objects

The workspace object system uses `BaseWorkspaceEntity` as the base class and decorates fields with `FieldMetadataType` enum values. New standard objects follow the pattern:
- Extend `BaseWorkspaceEntity` in a `.workspace-entity.ts` file.
- Declare relations using the `EntityRelation<T>` type.
- Register the module in the appropriate NestJS module with `@Processor` / `@Process` for any background jobs.
- Generate an instance command (`database:migrate:generate --name <name> --type fast|slow`) for schema changes.

### Adding a New Provider / External Connector

1. Add OAuth credentials and an enable flag to server environment (and Docker Compose env section).
2. Implement a `ConnectedAccount` variant by supplying `connectionParameters` (the `ImapSmtpCaldavParams` union or a new discriminant).
3. Register import and cleanup jobs on the appropriate `MessageQueue` channel.
4. Wire up `performSubscribe` / `performUnsubscribe` on the Zapier side if webhook-driven triggers are needed.

### Observability Add-ons

`packages/twenty-docker/` also ships Grafana dashboards, an OpenTelemetry Collector config (`otel-collector/`), Helm charts (`helm/`), Kubernetes manifests (`k8s/`), and Podman Compose files — providing operator-level extension points without changing application code.

---

## Summary

Twenty's external integration surfaces span a Zapier connector (webhook + CRUD actions over GraphQL), a generated type-safe TypeScript client SDK (per-application, with transparent token refresh), React Email-based transactional templates, and a connected-account system that syncs Gmail, Microsoft mail, and calendar data through dedicated BullMQ queues. Operationally, two Docker Compose files cover both local dev infrastructure (`docker-compose.dev.yml`) and full production stack (`docker-compose.yml`), with all provider integrations activated through environment variable feature flags rather than code changes.

---