Jesse A. Green / notebook

Twenty Admin Guide

Generated June 2026 from twentyhq/twenty at 979047d

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is the operational and architectural reference for Twenty, the open-source CRM. It is written for administrators, DevOps engineers, and developers who deploy, configure, secure, operate, and extend a Twenty instance — whether self-hosted or integrated against Twenty Cloud.

It covers the system architecture, deployment with Docker Compose, the complete configuration surface, security model, day-to-day operations (backups, upgrades, key rotation, background jobs), the REST/GraphQL API, OAuth and webhooks, the Apps extensibility platform, and developer internals. End-user product walkthroughs live in the companion User Guide.

Important

Twenty's authoritative configuration reference is the decorated class packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts. This guide documents that surface (169 variables across 20 groups) and how the runtime resolves them. Where a value can change between releases, treat the in-app Settings → Admin Panel → Configuration Variables page as the live source of truth.


1. Executive Summary

Twenty is a developer-first CRM that treats the data model as a first-class, user-extensible asset. Unlike traditional CRMs with fixed objects and a config panel, Twenty lets administrators and developers define custom objects, fields, relations, views, workflows, and AI agents — each of which automatically gains its own API, permissions, and UI.

Aspect Summary
What it is Open-source, self-hostable CRM platform
Language TypeScript end to end
Build system Nx monorepo
Backend NestJS, GraphQL Yoga, TypeORM, BullMQ
Datastores PostgreSQL (16), Redis
Frontend React, with Jotai (state), Linaria (CSS-in-JS), Lingui (i18n)
Deployment unit Docker Compose: server, worker, db, redis
Tenancy Workspace-based; single- or multi-workspace modes
APIs REST + GraphQL, schema-per-tenant (Core + Metadata)
Auth Password, Google/Microsoft OAuth SSO, SAML/OIDC, API keys, OAuth 2.0 (PKCE + client credentials)
Extensibility Apps (TypeScript packages: objects, logic functions, front components, agents)

Note

Twenty originally used Prisma but moved to TypeORM specifically to get the lower-level control needed to support user-defined custom fields and objects at runtime. This decision shapes the whole data layer.


2. Architecture

2.1 Container topology

A standard deployment is four containers, defined in packages/twenty-docker/docker-compose.yml:

                         ┌─────────────────────────────────┐
        Browser  ───────▶│  server  (twentycrm/twenty)      │
        / API client     │  NestJS + GraphQL Yoga + React   │
                         │  port 3000, /healthz healthcheck │
                         │  runs DB migrations + cron reg.  │
                         └───────┬─────────────┬────────────┘
                                 │             │
                  ┌──────────────▼──┐   ┌──────▼───────────────┐
                  │  db  postgres:16│   │ redis (noeviction)   │
                  │  vol: db-data   │   │ cache + BullMQ queue │
                  └──────────────▲──┘   └──────▲───────────────┘
                                 │             │
                         ┌───────┴─────────────┴────────────┐
                         │  worker  (twentycrm/twenty)       │
                         │  cmd: yarn worker:prod            │
                         │  DISABLE_DB_MIGRATIONS=true       │
                         │  DISABLE_CRON_JOBS_REGISTRATION=true│
                         └───────────────────────────────────┘
Service Image Role
server twentycrm/twenty:${TAG} HTTP API + GraphQL + serves the React frontend. Runs DB migrations on startup and registers cron jobs. Exposes port 3000, health-checked at /healthz.
worker twentycrm/twenty:${TAG} Runs background jobs via yarn worker:prod. Has DISABLE_DB_MIGRATIONS=true and DISABLE_CRON_JOBS_REGISTRATION=true because the server owns those.
db postgres:16 Primary datastore. Volume db-data. Default database name default.
redis redis Cache and message queue (BullMQ) backend. Started with --maxmemory-policy noeviction so queued jobs are never evicted.

Shared volume server-local-data is mounted into both server and worker at /app/packages/twenty-server/.local-storage for local file storage (used when STORAGE_TYPE=local).

Warning

Redis must run with maxmemory-policy noeviction. If Redis evicts keys under memory pressure, queued background jobs (email sync, calendar sync, workflows) can be silently lost.

2.2 The two database schemas

Provisioning creates two PostgreSQL schemas:

  • core / default — workspace records (People, Companies, custom objects, etc.).
  • metadata — the definition of each workspace's data model (objects, fields, relations).

If, after provisioning, you don't see both default and metadata schemas, you likely have more than one Postgres instance running. The metadata schema is what makes schema-per-tenant APIs possible: the API layer reads the metadata to generate REST/GraphQL endpoints that match each workspace's live model.

2.3 Request and data flow

  1. The server serves the React app and exposes REST (/rest/*) and GraphQL (/graphql, /metadata) endpoints.
  2. Mutations and queries run through TypeORM against PostgreSQL, scoped to the caller's workspace and role.
  3. Long-running work (email/calendar import, workflow execution, AI agent steps, cleanup) is enqueued on BullMQ (backed by Redis) and executed by the worker.
  4. Cron-scheduled jobs (registered by the server) drive recurring sync and maintenance.

2.4 Monorepo package map

Twenty is an Nx monorepo. The packages an administrator or integrator cares about most:

Package Purpose
twenty-server NestJS backend: API, auth, workspaces, metadata engine, workflows, jobs
twenty-front React frontend (the CRM app)
twenty-docker Docker Compose, Helm chart, k8s manifests, install scripts, Postgres image
twenty-cli / twenty-sdk The yarn twenty app-development CLI and SDK
create-twenty-app Scaffolder for new apps
twenty-ui / twenty-shared / twenty-utils Shared UI components and utilities
twenty-emails Transactional email templates
twenty-zapier Zapier integration
twenty-website / twenty-docs Marketing site and documentation

3. System Requirements

Requirement Minimum Notes
RAM 2 GB Insufficient memory causes process crashes
Docker & Docker Compose Current stable Both required; keep updated
PostgreSQL 16 (bundled) The Compose file ships postgres:16
Redis Current Must use noeviction policy
Disk Sized for db-data + uploads Use S3 for production file storage

Note

For local development (contributing to Twenty itself), use the local setup with Node and Yarn rather than the production containers. This guide focuses on production/self-hosting; contributor setup is in the project's developers/contribute docs.


4. Deployment (Docker Compose)

Warning

The Docker images are for production hosting / self-hosting. For contributing to Twenty's source, use the local setup instead.

4.1 One-line install

Install the latest stable version:

bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)

To pin a version or branch:

VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)

4.2 Manual setup

Step 1 — Create the .env file.

curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example

The example .env:

TAG=latest

#PG_DATABASE_USER=postgres
#PG_DATABASE_PASSWORD=replace_me_with_a_strong_password_without_special_characters
#PG_DATABASE_HOST=db
#PG_DATABASE_PORT=5432
#REDIS_URL=redis://redis:6379

SERVER_URL=http://localhost:3000

# Use openssl rand -base64 32 for each secret
# ENCRYPTION_KEY=replace_me_with_a_random_string
# FALLBACK_ENCRYPTION_KEY= # set to the previous ENCRYPTION_KEY during a rotation
# APP_SECRET= # legacy: only required for instances that pre-date ENCRYPTION_KEY

STORAGE_TYPE=local

# STORAGE_S3_REGION=eu-west3
# STORAGE_S3_NAME=my-bucket
# STORAGE_S3_ENDPOINT=
# STORAGE_S3_ACCESS_KEY_ID=
# STORAGE_S3_SECRET_ACCESS_KEY=

Step 2 — Generate an encryption key.

openssl rand -base64 32

Put the result in ENCRYPTION_KEY.

Caution

Losing ENCRYPTION_KEY means losing access to every secret in the database — OAuth tokens, application variables, signing-key private keys, sensitive config values, and TOTP secrets. Store it in a secrets manager. Set a dedicated ENCRYPTION_KEY before first boot (and especially before upgrading to v2.5+, see §9.3).

Step 3 — Set the Postgres password. Update PG_DATABASE_PASSWORD with a strong password without special characters.

Step 4 — Get the Compose file.

curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml

Step 5 — Launch.

docker compose up -d

Step 6 — Access. Visit http://localhost:3000, or on a server:

curl http://localhost:3000

The server runs all required upgrade migrations automatically on startup.

4.3 Exposing Twenty externally — SERVER_URL

By default Twenty runs on localhost:3000. To serve it on a domain, set SERVER_URL to exactly the URL users will use in their browser.

Direct access (no proxy)

SERVER_URL=http://your-domain-or-ip:3000

Reverse proxy + SSL (standard ports)

SERVER_URL=https://your-domain-or-ip

Reverse proxy + custom port

SERVER_URL=https://your-domain-or-ip:custom-port

Restart after changing it:

docker compose down
docker compose up -d

Important

SSL is required for some browser features (for example, the clipboard API used by copy buttons). Run Twenty behind a reverse proxy (Nginx, Traefik, Caddy) with SSL termination forwarding to internal port 3000, and ensure X-Forwarded-For and X-Forwarded-Proto headers are set. SERVER_URL must match the external URL exactly.

4.4 Persistence

The Compose configuration uses named volumes (db-data, server-local-data) to persist the database and local file storage. On stateless platforms, use external storage (S3) for uploads so files survive container restarts.


5. Configuration

5.1 Two configuration modes

Twenty resolves most configuration from either the database (admin panel) or environment variables, controlled by one flag:

Mode Flag Behavior
Admin Panel (default) IS_CONFIG_VARIABLES_IN_DB_ENABLED=true Most settings managed in the UI at Settings → Admin Panel → Configuration Variables. Changes take effect immediately (within ~15 s across multiple containers, since server and worker read the same DB).
Environment-only IS_CONFIG_VARIABLES_IN_DB_ENABLED=false All settings come from .env. The admin panel shows values read-only. Restart to apply.

Only users with canAccessFullAdminPanel: true can reach the admin configuration UI.

Important

Some settings are env-only regardless of mode because they're infrastructure or secrets: PG_DATABASE_URL, SERVER_URL, REDIS_URL, ENCRYPTION_KEY, FALLBACK_ENCRYPTION_KEY, APP_SECRET, and IS_MULTIWORKSPACE_ENABLED. These can only be set in .env and require a restart.

5.2 Configuration variable reference

The server defines 169 configuration variables grouped into 20 categories. The tables below list the key variables per group with their code defaults. The complete, always-current list with descriptions is in config-variables.ts and in the admin panel.

Server config (SERVER_CONFIG)

Variable Default Purpose
SERVER_URL http://localhost:3000 Public URL of the server
FRONTEND_URL — Frontend base URL
NODE_ENV production Runtime environment
NODE_PORT 3000 Listen port
PG_DATABASE_URL — Postgres connection string (env-only)
REDIS_URL — Redis connection string (env-only)
IS_MULTIWORKSPACE_ENABLED false Multi-tenant mode (env-only)
DEFAULT_SUBDOMAIN app Login subdomain in multi-workspace mode
IS_CONFIG_VARIABLES_IN_DB_ENABLED true DB vs env config mode
APP_SECRET — Legacy at-rest/session secret
ENCRYPTION_KEY — At-rest encryption key
FALLBACK_ENCRYPTION_KEY — Previous key, used during rotation
TRUST_PROXY loopback, linklocal, uniquelocal Proxy trust setting
SERVER_ID / PUBLIC_DOMAIN_URL — Instance identity / public domain
ENTERPRISE_KEY / ENTERPRISE_API_URL — / https://twenty.com/api/enterprise Enterprise licensing
CACHE_STORAGE_TTL 7 days Cache TTL

Storage (STORAGE_CONFIG)

Variable Default Purpose
STORAGE_TYPE local local or S_3
STORAGE_LOCAL_PATH .local-storage Local storage dir
STORAGE_S3_REGION / STORAGE_S3_NAME / STORAGE_S3_ENDPOINT — S3 bucket settings
STORAGE_S3_ACCESS_KEY_ID / STORAGE_S3_SECRET_ACCESS_KEY — S3 credentials
STORAGE_S3_PRESIGNED_URL_ENABLED false Use presigned URLs
STORAGE_S3_PRESIGNED_URL_EXPIRES_IN 900 Presigned URL lifetime (s)
MAX_TARBALL_UPLOAD_SIZE_BYTES 100 MB App tarball upload cap

Authentication — Google (GOOGLE_AUTH) and Microsoft (MICROSOFT_AUTH)

Variable Default Purpose
AUTH_GOOGLE_ENABLED false Enable Google SSO
AUTH_GOOGLE_CLIENT_ID / AUTH_GOOGLE_CLIENT_SECRET — Google OAuth credentials
AUTH_GOOGLE_CALLBACK_URL / AUTH_GOOGLE_APIS_CALLBACK_URL — SSO / integration callbacks
MESSAGING_PROVIDER_GMAIL_ENABLED false Gmail message sync
CALENDAR_PROVIDER_GOOGLE_ENABLED false Google Calendar sync
AUTH_MICROSOFT_ENABLED false Enable Microsoft SSO
AUTH_MICROSOFT_CLIENT_ID / AUTH_MICROSOFT_CLIENT_SECRET — Microsoft OAuth credentials
AUTH_MICROSOFT_CALLBACK_URL / AUTH_MICROSOFT_APIS_CALLBACK_URL — SSO / integration callbacks
MESSAGING_PROVIDER_MICROSOFT_ENABLED / CALENDAR_PROVIDER_MICROSOFT_ENABLED false Microsoft mail / calendar sync

Related auth flags (in ADVANCED_SETTINGS): AUTH_PASSWORD_ENABLED (true), IS_EMAIL_VERIFICATION_REQUIRED (false), IS_IMAP_SMTP_CALDAV_ENABLED (true).

Email (EMAIL_SETTINGS)

Variable Default Purpose
EMAIL_DRIVER LOGGER LOGGER (logs only) or smtp
EMAIL_FROM_ADDRESS [email removed] From address
EMAIL_FROM_NAME Felix from Twenty From display name
EMAIL_SMTP_HOST / EMAIL_SMTP_PORT — / 587 SMTP server
EMAIL_SMTP_USER / EMAIL_SMTP_PASSWORD — SMTP credentials
EMAIL_SMTP_NO_TLS false Disable TLS

Tokens & durations (TOKENS_DURATION)

Variable Default
ACCESS_TOKEN_EXPIRES_IN 30m
REFRESH_TOKEN_EXPIRES_IN 60d
LOGIN_TOKEN_EXPIRES_IN 15m
FILE_TOKEN_EXPIRES_IN 1d
INVITATION_TOKEN_EXPIRES_IN 30d
PASSWORD_RESET_TOKEN_EXPIRES_IN 5m
EMAIL_VERIFICATION_TOKEN_EXPIRES_IN 1h
APPLICATION_ACCESS_TOKEN_EXPIRES_IN / APPLICATION_REFRESH_TOKEN_EXPIRES_IN 30m / 60d

Rate limiting (RATE_LIMITING)

Variable Default Purpose
API_RATE_LIMITING_SHORT_LIMIT / API_RATE_LIMITING_SHORT_TTL_IN_MS 100 / 1000 Per-second API cap
API_RATE_LIMITING_LONG_LIMIT / API_RATE_LIMITING_LONG_TTL_IN_MS 100 / 60000 Per-minute API cap
MUTATION_MAXIMUM_AFFECTED_RECORDS 100 Max records per mutation
GRAPHQL_MAX_FIELDS / GRAPHQL_MAX_ROOT_RESOLVERS 2000 / 20 GraphQL query limits
COMMON_QUERY_COMPLEXITY_LIMIT 2000 Query complexity cap
WORKFLOW_EXEC_SOFT_THROTTLE_LIMIT / WORKFLOW_EXEC_HARD_THROTTLE_LIMIT 100 / 5000 Workflow execution throttles
INVITATION_SENDING_BY_WORKSPACE_THROTTLE_LIMIT 500 / 7 days Invitation throttle

Logic functions & code interpreter

Variable Default Purpose
LOGIC_FUNCTION_TYPE DISABLED in prod / LOCAL in dev DISABLED, LOCAL, or LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION / _ROLE / _ACCESS_KEY_ID / _SECRET_ACCESS_KEY — AWS Lambda sandbox
LOGIC_FUNCTION_EXEC_THROTTLE_LIMIT / _TTL 1000 / 60000 Execution throttle
CODE_INTERPRETER_TYPE DISABLED in prod / LOCAL in dev DISABLED, LOCAL, or E_2_B
CODE_INTERPRETER_TIMEOUT_MS 300000 Execution timeout

Other groups

  • BILLING_CONFIG — IS_BILLING_ENABLED (false), BILLING_STRIPE_API_KEY, BILLING_STRIPE_WEBHOOK_SECRET, trial durations (30/7 days), free workflow credits, BILLING_PLAN_REQUIRED_LINK.
  • LLM — AI_PROVIDERS, AI_MODELS_DEFAULT_FAST / _SMART / _RECOMMENDED / _DISABLED. Configure AI providers and default models here.
  • LOGGING — LOG_LEVELS (['log','error','warn']), LOGGER_DRIVER (CONSOLE), EXCEPTION_HANDLER_DRIVER, SENTRY_DSN / SENTRY_FRONT_DSN / SENTRY_ENVIRONMENT, TELEMETRY_ENABLED (true), METER_DRIVER.
  • ANALYTICS_CONFIG — ANALYTICS_ENABLED (false), CLICKHOUSE_URL.
  • SSL — SSL_KEY_PATH, SSL_CERT_PATH (for terminating TLS directly at the server).
  • CLOUDFLARE_CONFIG — CLOUDFLARE_API_KEY, CLOUDFLARE_ZONE_ID, etc. (custom-domain automation).
  • CAPTCHA_CONFIG — CAPTCHA provider/site/secret keys.
  • SUPPORT_CHAT_CONFIG — SUPPORT_DRIVER (NONE), Front chat keys.
  • AWS_SES_SETTINGS — AWS_SES_REGION, credentials, INBOUND_EMAIL_DOMAIN, SES_SNS_TOPIC_ARN_ALLOWLIST.
  • ADVANCED_SETTINGS — workspace lifecycle (WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION/_SOFT_DELETION/_DELETION = 7/14/21), OUTBOUND_HTTP_SAFE_MODE_ENABLED (true), WORKSPACE_SCHEMA_DDL_LOCKED (false), Postgres pool tuning (PG_POOL_MAX_CONNECTIONS=10, PG_POOL_IDLE_TIMEOUT_MS=600000), APP_REGISTRY_URL (https://registry.npmjs.org), maps/autocomplete keys.

5.3 Multi-workspace mode

By default Twenty runs single-workspace — ideal for one organization.

Mode Setting Behavior
Single (default) IS_MULTIWORKSPACE_ENABLED=false One workspace; first user becomes admin (canImpersonate, canAccessFullAdminPanel); new signups disabled after the first workspace; URL = https://your-domain.com
Multi (SaaS-like) IS_MULTIWORKSPACE_ENABLED=true, DEFAULT_SUBDOMAIN=app Multiple workspaces, each on its own subdomain; users sign up/log in at app.your-domain.com; no automatic admin

For multi-workspace, configure wildcard DNS so new subdomains route automatically:

*.your-domain.com -> your-server-ip

Restrict who can create workspaces with IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true.

Warning

IS_MULTIWORKSPACE_ENABLED is env-only and requires a restart; it cannot be toggled from the admin panel.

5.4 Google Workspace integration

  1. In Google Cloud Console, create a project and enable the Gmail API, Google Calendar API, and People API.
  2. Create an OAuth 2.0 Client ID and add redirect URIs:
    • https://{your-domain}/auth/google/redirect (SSO)
    • https://{your-domain}/auth/google-apis/get-access-token (integrations)
  3. In Settings → Admin Panel → Configuration Variables → Google Auth, set:
MESSAGING_PROVIDER_GMAIL_ENABLED=true
CALENDAR_PROVIDER_GOOGLE_ENABLED=true
AUTH_GOOGLE_CLIENT_ID={client-id}
AUTH_GOOGLE_CLIENT_SECRET={client-secret}
AUTH_GOOGLE_CALLBACK_URL=https://{your-domain}/auth/google/redirect
AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token

Required scopes (configured automatically): calendar.events, gmail.readonly, profile.emails.read.

5.5 Microsoft 365 integration

Important

Each user needs a Microsoft 365 license to sync calendar and mail. Personal Microsoft accounts are not supported (error AADSTS50020).

  1. Create a project in Microsoft Azure and obtain credentials.
  2. Grant Microsoft Graph permissions: Mail.ReadWrite, Mail.Send (only needed to send mail from workflow actions — Mail.Read suffices for receive-only), Calendars.Read, User.Read, openid, email, profile, offline_access.
  3. Add redirect URIs: https://{your-domain}/auth/microsoft/redirect (SSO) and https://{your-domain}/auth/microsoft-apis/get-access-token.
  4. Set the AUTH_MICROSOFT_* and provider-enabled variables (mirroring the Google block above).

5.6 Email (SMTP)

Set EMAIL_DRIVER=smtp and the SMTP variables. The default LOGGER driver only logs emails — switch to smtp to actually send.

Gmail

EMAIL_DRIVER=smtp
EMAIL_SMTP_HOST=smtp.gmail.com
EMAIL_SMTP_PORT=465
EMAIL_SMTP_USER=gmail_email_address
EMAIL_SMTP_PASSWORD='gmail_app_password'

Office 365

EMAIL_DRIVER=smtp
EMAIL_SMTP_HOST=smtp.office365.com
EMAIL_SMTP_PORT=587
EMAIL_SMTP_USER=office365_email_address
EMAIL_SMTP_PASSWORD='office365_password'

smtp4dev (testing)

docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev
EMAIL_DRIVER=smtp
EMAIL_SMTP_HOST=localhost
EMAIL_SMTP_PORT=2525

Gmail and Office 365 require an app password when 2FA is enabled.

5.7 S3 storage

For production, store uploads in S3 (or S3-compatible MinIO / DigitalOcean Spaces) so files persist across restarts and scale across server replicas:

STORAGE_TYPE=S_3
STORAGE_S3_REGION=...
STORAGE_S3_NAME=...
STORAGE_S3_ENDPOINT=...
STORAGE_S3_ACCESS_KEY_ID=...
STORAGE_S3_SECRET_ACCESS_KEY=...

Tip

When using S3 with in-browser file downloads, configure the bucket's CORS to allow your Twenty frontend origin, or downloads that depend on CORS will fail.

5.8 Logic functions & code interpreter (security-sensitive)

Both run user-provided code, so they default to DISABLED in production.

Feature Driver options Notes
Logic functions DISABLED / LOCAL / LAMBDA LOCAL runs on the host with no sandboxing — trusted code only. LAMBDA gives hardware-level isolation for production.
Code interpreter DISABLED / LOCAL / E_2_B LOCAL is dev-only. E_2_B gives sandboxed execution for production.

Production AWS example:

LOGIC_FUNCTION_TYPE=LAMBDA
LOGIC_FUNCTION_LAMBDA_REGION=us-east-1
LOGIC_FUNCTION_LAMBDA_ROLE=arn:aws:iam::123456789:role/your-lambda-role
LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID=your-access-key
LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY=your-secret-key

Caution

Never run LOGIC_FUNCTION_TYPE=LOCAL or CODE_INTERPRETER_TYPE=LOCAL in production with untrusted code — the code executes directly on the host with no isolation. Use LAMBDA / E_2_B, or keep these disabled.


6. Security

6.1 Encryption at rest

Twenty encrypts secrets at rest inside a versioned envelope: every encrypted value is stored as enc:v2:<keyId>:<payload>, where <keyId> is an 8-hex prefix derived from ENCRYPTION_KEY. Six sites are encrypted: connected-account tokens, application variables, application-registration variables, signing-key private keys, sensitive config storage, and TOTP secrets.

If ENCRYPTION_KEY is unset, Twenty falls back to the legacy APP_SECRET for at-rest encryption (and session-cookie signing). New installs should always set a dedicated ENCRYPTION_KEY; APP_SECRET is deprecated and kept only for backward compatibility and to verify legacy HS256 tokens.

6.2 JWT signing keys

Access and refresh tokens are signed with asymmetric ES256 keypairs stored in core."signingKey", each tagged with a kid. Exactly one key is isCurrent; public keys are retained indefinitely so previously issued tokens still verify. See §9.4 for rotation and revocation.

6.3 Authentication options

Method Use
Password AUTH_PASSWORD_ENABLED=true (default)
Google / Microsoft SSO OAuth (see §5.4–5.5)
SAML / OIDC Enterprise SSO, configured per workspace
API keys Authorization: Bearer <key>, scoped to a role
OAuth 2.0 Authorization code + PKCE, and client credentials (see §11)

6.4 Network & request safety

  • OUTBOUND_HTTP_SAFE_MODE_ENABLED=true (default) guards outbound HTTP from workflow/logic actions against hitting internal addresses (SSRF protection).
  • Rate limiting (§5.2) caps API and workflow execution.
  • TRUST_PROXY controls which proxy hops are trusted for client IP resolution — set it correctly behind your reverse proxy.

6.5 Permissions model

Role-based access control applies at object, field, and record level. API keys are scoped to a role (Settings → Members → Roles → Assignment), so a key can be limited to exactly what it needs. AI agents act under a role and cannot exceed it.

Important

Treat workspace data ownership as a feature: self-hosting keeps all CRM data on your infrastructure for compliance and data-residency requirements. Combine that with scoped roles, sandboxed logic functions, and rotated keys for a defensible posture.


7. Operations

7.1 Lifecycle commands

docker compose up -d        # start
docker compose down         # stop
docker compose logs         # view logs
docker compose logs -f server   # follow the server
docker compose ps           # status

7.2 Server admin commands

The server ships a command CLI (run inside the server or worker container via yarn command:prod <name>, or npx nx run twenty-server:command:prod <name>). Major commands:

Command Purpose
upgrade:status Inspect instance + per-workspace migration state
secret-encryption:rotate Re-encrypt at-rest secrets under a new key (§9.4)
workspace:generate-api-key Generate an API key for a workspace
workspace:export Export a workspace
workspace:clean / workspace:clean:onboarding Clean up workspaces
workspace:destroy Permanently destroy a workspace
workspace:list-and-delete-orphaned-entities Integrity cleanup
cache:flush / cache:flat-cache-invalidate Clear caches

7.3 Background (cron) jobs

The server registers recurring jobs; the worker executes them. After configuring Gmail / Google Calendar / Microsoft 365, register the sync jobs in the worker:

yarn command:prod cron:messaging:messages-import
yarn command:prod cron:messaging:message-list-fetch
yarn command:prod cron:calendar:calendar-event-list-fetch
yarn command:prod cron:calendar:calendar-events-import
yarn command:prod cron:messaging:ongoing-stale
yarn command:prod cron:calendar:ongoing-stale
yarn command:prod cron:workflow:automated-cron-trigger

Other registered cron jobs include: cron:register:all, cron:trash-cleanup, cron:event-log-cleanup, cron:stale-registration-cleanup, cron:clean-onboarding-workspaces, cron:clean-suspended-workspaces, cron:rotate-signing-keys, cron:enterprise-key-validation, cron:marketplace-catalog-sync, cron:app-version-check, cron:workflow:clean-workflow-runs, cron:workflow:handle-staled-runs, cron:workflow:enqueue-awaiting-workflow-run, cron:public-domain:check-public-domains-valid-records, and the cron:messaging:* / cron:calendar:* relaunch-failed-channel jobs.

Tip

A quick way to confirm the worker is alive: create a webhook (Settings → enable Advanced → Webhooks) pointing at a request-bin URL filtered to Companies + Created, then create a company. If the POST arrives, the worker is running.

7.4 Backup and restore

Single-database dump:

docker exec twenty-postgres pg_dump -U postgres twenty > backup_$(date +%Y%m%d).sql

Automated daily backup (crontab):

0 2 * * * docker exec twenty-postgres pg_dump -U postgres twenty > /backups/twenty_$(date +\%Y\%m\%d).sql

Full-cluster dump (recommended before upgrades):

docker exec -it {db_container} pg_dumpall -U {postgres_user} > databases_backup.sql

Restore:

docker compose stop server worker
cat databases_backup.sql | docker exec -i {db_container} psql -U {postgres_user}
docker compose up -d

Tip

Test restores regularly, store backups off-site (S3/GCS), encrypt them, and keep daily/weekly/monthly retention tiers.

7.5 Health & observability

  • Health endpoint: GET /healthz (used by the Compose healthcheck).
  • Errors: configure SENTRY_DSN / SENTRY_FRONT_DSN / SENTRY_ENVIRONMENT.
  • Metrics: METER_DRIVER and OTLP_COLLECTOR_METRICS_ENDPOINT_URL (OpenTelemetry collector config ships under twenty-docker/otel-collector and twenty-docker/grafana).
  • HEALTH_METRICS_TIME_WINDOW_IN_MINUTES (5) controls the health metrics window.

8. Operations Checklist

  • [ ] .env created from .env.example
  • [ ] ENCRYPTION_KEY generated with openssl rand -base64 32 and stored in a secrets manager
  • [ ] PG_DATABASE_PASSWORD set to a strong value (no special characters)
  • [ ] SERVER_URL matches the external browser URL
  • [ ] Reverse proxy with SSL termination configured; X-Forwarded-* headers passed
  • [ ] STORAGE_TYPE=S_3 configured for production uploads
  • [ ] EMAIL_DRIVER=smtp configured (or LOGGER intentionally)
  • [ ] OAuth (Google/Microsoft) configured if email/calendar sync is needed
  • [ ] Sync cron jobs registered in the worker
  • [ ] Logic functions / code interpreter set to DISABLED or a sandboxed driver
  • [ ] Automated database backups scheduled and a restore tested
  • [ ] Redis confirmed running with noeviction
  • [ ] Admin user has canAccessFullAdminPanel

9. Upgrades & Key Rotation

9.1 Standard upgrade

docker compose down
# edit TAG in .env to the new version
docker compose up -d

The server runs required migrations automatically on startup. No manual migration command is needed.

9.2 Cross-version upgrades

From v1.22+, Twenty supports cross-version upgrades — jump directly from any supported version to the latest (for example, v1.22 → v2.0). Before v1.22, upgrade incrementally through each major tag (v1.6 → v1.7 → …) until you reach v1.22, then jump.

9.3 Upgrading to v2.5+ (encryption envelope)

From v2.5, at-rest secrets move into the enc:v2: envelope. First boot on v2.5 runs idempotent backfill commands that rewrite existing rows. They resume if interrupted but can take a while on large databases — monitor with upgrade:status.

Important

Set a dedicated ENCRYPTION_KEY before the v2.5 upgrade so the backfill writes under it from the start. Switching keys afterward requires a full rotation (§9.4).

9.4 Checking upgrade status

docker exec -it {server_container} yarn command:prod upgrade:status
APP_VERSION: v1.23.0

Instance
    Inferred version: 1.23.0
    Status:           Up to date

Summary
    Instance: Up to date
    Workspaces: 1 up to date, 0 behind, 0 failed (1 total)
Flag Description
-w, --workspace-id <id> Filter to a workspace (repeatable)
-f, --failed-only Show only behind/failed workspaces and their errors

If an upgrade fails on some workspaces, the server won't advance past the failing step; restarting retries from where it left off. Use --failed-only to find and read the errors.

9.5 Rotating ENCRYPTION_KEY

Online and resumable (the secret-encryption:rotate command ships in v2.6+):

  1. Generate a new key: openssl rand -base64 32.
  2. Configure both keys and restart — new writes use the new key, old rows decrypt via the fallback:
    ENCRYPTION_KEY=NEW_VALUE
    FALLBACK_ENCRYPTION_KEY=OLD_VALUE
    
  3. Re-encrypt existing rows:
    docker exec -it {server_container} yarn command:prod secret-encryption:rotate
    
    Flag Description
    -s, --site <site> Limit to one site
    -b, --batch-size <n> Rows per batch (default 200, max 5000)
    -d, --dry-run Decrypt + re-encrypt in memory, skip the UPDATE
  4. When a --dry-run shows zero remaining rows, remove FALLBACK_ENCRYPTION_KEY and restart.

The command is idempotent (a SQL filter skips rows already on the new keyId) — interrupt and re-run freely. It exits non-zero if any row fails.

9.6 Rotating / revoking JWT signing keys

  • Auto-rotation (v2.6+): set SIGNING_KEY_ROTATION_DAYS to issue a new current key once the existing one exceeds that age (via the cron:rotate-signing-keys job). Previous keys aren't revoked, so existing tokens keep verifying. Leave unset to disable.
  • Emergency revoke: Settings → Admin Panel → Signing keys → Revoke on a non-current row. This wipes the encrypted private material, sets revokedAt, and rejects every token signed under that kid.

10. The API

10.1 Schema-per-tenant

There is no static API reference. Each workspace has its own schema; when you add a custom object (say Invoice), it immediately gets REST and GraphQL endpoints identical to built-in objects — using your object and field names directly (no opaque IDs). Your workspace-specific docs and an interactive playground are at Settings → API & Webhooks after creating an API key.

10.2 Two APIs

API REST GraphQL Purpose
Core API /rest/ /graphql/ CRUD on records (People, Companies, custom objects); query, filter, traverse relations
Metadata API /rest/metadata/ /metadata/ Manage the schema: create/modify/delete objects, fields, relations

GraphQL adds batch upserts and single-query relation traversal. Same underlying data either way.

10.3 Base URLs and auth

Environment Base URL
Cloud https://api.twenty.com/
Self-hosted https://{your-domain}/
Authorization: Bearer YOUR_API_KEY

Create a key in Settings → API & Webhooks → + Create key (shown once — copy immediately). Scope it to a role to limit access.

10.4 Batching and rate limits

Limit Value
Requests 100 per minute
Batch size 60 records per call (create/update/delete)

GraphQL also supports batch upsert using plural names (e.g. CreateCompanies).


11. OAuth 2.0

Twenty implements OAuth 2.0 with authorization code + PKCE for user-facing apps and client credentials for server-to-server access. Clients register dynamically per RFC 7591 — no dashboard setup.

Scenario Method
Internal scripts/automation API key
External app on behalf of a user OAuth — authorization code
Server-to-server, no user OAuth — client credentials
Twenty App with UI extensions Apps (OAuth handled automatically)

11.1 Endpoints

Endpoint Purpose
/.well-known/oauth-authorization-server Server metadata discovery
/oauth/register Dynamic client registration
/oauth/authorize User authorization
/oauth/token Token exchange & refresh

11.2 Register a client

POST /oauth/register
Content-Type: application/json

{
  "client_name": "My Integration",
  "redirect_uris": ["https://myapp.com/callback"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "client_secret_post"
}

Warning

Store the returned client_secret securely — it cannot be retrieved later.

11.3 Scopes

Scope Access
api Full read/write to Core + Metadata APIs
profile Read the authenticated user's profile

Request as a space-separated string: scope=api profile.

11.4 Authorization code flow

  1. Redirect the user to /oauth/authorize with client_id, response_type=code, redirect_uri, scope, a random state, and a PKCE code_challenge (code_challenge_method=S256).
  2. Handle the callback at your redirect_uri?code=...&state=...; verify state.
  3. Exchange the code at /oauth/token with grant_type=authorization_code, the code, redirect_uri, client credentials, and code_verifier. You receive access_token, refresh_token, and expires_in (3600 s).
  4. Call the API with Authorization: Bearer ACCESS_TOKEN.
  5. Refresh with grant_type=refresh_token.

11.5 Client credentials flow

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=...&client_secret=...&scope=api

The token has workspace-level access, not tied to a user.


12. Webhooks

Twenty POSTs to your URL whenever a record is created, updated, or deleted — for all object types, including custom objects.

12.1 Create

Settings → APIs & Webhooks → Webhooks → + Create webhook → enter a publicly reachable URL → Save. It activates immediately.

12.2 Events & payload

Event type Examples
Created person.created, company.created, note.created
Updated person.updated, opportunity.updated
Deleted person.deleted, company.deleted
{
  "event": "person.created",
  "data": { "id": "abc12345", "firstName": "Alice", "lastName": "Doe", "email": "[email removed]", "createdAt": "2025-02-10T15:30:45Z", "createdBy": "user_123" },
  "timestamp": "2025-02-10T15:30:50Z"
}

Respond with a 2xx status to acknowledge; non-2xx responses are logged as delivery failures.

12.3 Signature validation

Each request is signed:

Header Meaning
X-Twenty-Webhook-Signature HMAC SHA256 signature
X-Twenty-Webhook-Timestamp Request timestamp

Validate by computing HMAC_SHA256(secret, "{timestamp}:{payload}") and comparing (constant-time) to the signature header:

const crypto = require("crypto");
const timestamp = req.headers["x-twenty-webhook-timestamp"];
const payload = JSON.stringify(req.body);
const stringToSign = `${timestamp}:${payload}`;
const expected = crypto.createHmac("sha256", secret).update(stringToSign).digest("hex");
const isValid = crypto.timingSafeEqual(
  Buffer.from(expected, "hex"),
  Buffer.from(req.headers["x-twenty-webhook-signature"], "hex")
);

12.4 Webhooks vs workflows

Method Direction Use case
Webhooks OUT Notify external systems of any record change
Workflow + HTTP Request OUT Send data out with filters/transformations
Workflow Webhook Trigger IN Receive data into Twenty

13. Extending Twenty — Apps

Apps are TypeScript packages (built with twenty-sdk) that extend Twenty with code: objects/fields, server-side logic, sandboxed UI, and AI capabilities.

13.1 What an app defines

Entity What it does
Objects & Fields New tables/fields, first-class like built-ins
Logic Functions Server-side TypeScript triggered by HTTP routes, cron, or DB events
Front Components Sandboxed React in Web Workers via Remote DOM (not iframes)
Skills & Agents AI instructions and autonomous assistants
Views & Navigation Pre-configured list views and sidebar items

Entities are detected by AST analysis at build time — put export default defineObject(...) in any .ts file and the SDK picks it up. Logic functions run in isolated Node processes via a typed API client scoped to the app's role; permissions are enforced at the API level.

13.2 The app-development CLI (yarn twenty)

Distinct from the server command CLI. Key commands:

Command Purpose
npx create-twenty-app my-app Scaffold a new app
yarn twenty dev Live-sync source to a running server
yarn twenty dev:build [--tarball] Build to .twenty/output/, optional .tgz
yarn twenty dev:function:exec -n <name> [-p '{...}'] Run a logic function manually
yarn twenty dev:function:logs [-n <name>] Stream function execution logs
yarn twenty app:publish Publish to npm / marketplace
yarn twenty app:publish --private [--remote <name>] Deploy a tarball to a server
yarn twenty app:install [-r <remote>] / app:uninstall [-y] Manage installs
yarn twenty remote:add [--local] [--url ... --api-key ...] [--as <name>] Manage remotes
yarn twenty remote:list / remote:use <name> List / select remote
yarn twenty dev:catalog-sync [--remote <name>] Force marketplace catalog sync

Remote credentials are stored in ~/.twenty/config.json.

13.3 Distribution

  • Tarball (private): yarn twenty app:publish --private uploads to a server. Sharing a private app across workspaces is an Enterprise feature. Version in package.json must be strictly higher than the deployed version (VERSION_ALREADY_EXISTS otherwise).
  • npm marketplace: add the twenty-app keyword to package.json and run yarn twenty app:publish. The server syncs the npm catalog hourly (or trigger dev:catalog-sync).
  • Server compatibility: declare engines.twenty (a semver range, e.g. >=2.3.0) in package.json; the server rejects installs that don't satisfy it (SERVER_VERSION_INCOMPATIBLE). MAX_TARBALL_UPLOAD_SIZE_BYTES caps tarball size (100 MB default).

Scaffolded apps ship GitHub Actions for CI (ephemeral test instance) and CD (deploy on push to main, needs TWENTY_DEPLOY_URL and a TWENTY_DEPLOY_API_KEY secret).


14. Developer Internals

14.1 Backend commands (contributor)

Run from packages/twenty-server (or npx nx run twenty-server:<command>):

npx nx run twenty-server:database:reset            # set up DB with dev seeds
npx nx run twenty-server:start                     # start the server
npx nx run twenty-server:worker                    # start the worker
npx nx run twenty-server:lint                      # lint (--fix to autofix)
npx nx run twenty-server:test:unit                 # unit tests
npx nx run twenty-server:test:integration          # integration tests
npx nx run twenty-server:database:migrate:generate # generate a TypeORM migration

14.2 Tech stack

  • Backend: NestJS, TypeORM, GraphQL Yoga (Twenty moved off Prisma to TypeORM for custom-object/field support).
  • Database: PostgreSQL.
  • Queue: BullMQ on Redis.
  • Testing: Jest. Tooling: Yarn, Oxlint (Oxc). Errors: Sentry.
  • Frontend: React, Jotai, Linaria, Lingui.

14.3 Repository layout (high level)

packages/
  twenty-server/    NestJS backend (API, auth, metadata, workflows, jobs, config-variables.ts)
  twenty-front/     React app
  twenty-docker/    Compose, Helm, k8s, install scripts, postgres image, otel/grafana
  twenty-cli/ twenty-sdk/ create-twenty-app/   App-dev tooling
  twenty-ui/ twenty-shared/ twenty-utils/      Shared libraries
  twenty-emails/    Email templates
  twenty-zapier/    Zapier integration
  twenty-docs/ twenty-website/                  Docs + marketing

15. Troubleshooting

password authentication failed for user "postgres" on first install

The first-boot password is baked into the DB volume. For fresh installs only (this deletes data):

# update PG_DATABASE_PASSWORD in .env first
docker compose down --volumes
docker compose up -d

Caution

Never run down --volumes on an instance with production data — it permanently deletes the database.

Admin panel not accessible

Grant yourself admin in the DB:

UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = '[email removed]';

Missing metadata schema after provisioning

Ensure only one Postgres instance is running on the host — a second instance causes provisioning to target the wrong database.

Workflow run fails: "Logic function execution is disabled…"

Logic functions default to disabled in production. Set LOGIC_FUNCTION_TYPE=LOCAL (trusted only) or LAMBDA via env or the admin panel (§5.8).

No emails sent / worker not running

Confirm the worker container is up. In dev, npx nx worker twenty-server. Verify with the webhook test in §7.3. Also check EMAIL_DRIVER is smtp (the default LOGGER only logs).

Connection issues behind a reverse proxy

Verify SERVER_URL matches the external URL (with https if SSL), that the proxy forwards to internal port 3000, and that X-Forwarded-For / X-Forwarded-Proto are set. Restart proxy and containers after changes.

Image upload fails: permission denied

Fix ownership of the data folder on the host (switch it off root to the user/group the container runs as), or move to S3 storage.

Can't log in after a 1-click setup

docker exec -it twenty-server-1 yarn
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
docker compose down && docker compose up -d

Caution

database:reset erases and recreates the database.

Out-of-memory on dev start

Run only the services you need (e.g. npx nx worker twenty-server) and raise the Node heap: export NODE_OPTIONS="--max-old-space-size=8192".


16. Quick Reference

Item Value
Containers server (3000), worker, db (postgres:16), redis
Health check GET /healthz
Default DB default (schemas: default + metadata)
Core API /rest/, /graphql/
Metadata API /rest/metadata/, /metadata/
OAuth discovery /.well-known/oauth-authorization-server
API auth Authorization: Bearer <key>
API rate limit 100/min; batch 60
Config reference config-variables.ts (169 vars, 20 groups)
Config mode flag IS_CONFIG_VARIABLES_IN_DB_ENABLED (default true)
At-rest key ENCRYPTION_KEY (+ FALLBACK_ENCRYPTION_KEY during rotation)
Upgrade flow edit TAG → docker compose up -d (auto-migrate)
Cross-version upgrades supported from v1.22+
App-dev CLI yarn twenty …
Server ops CLI yarn command:prod …
Stack NestJS · TypeORM · GraphQL Yoga · BullMQ · PostgreSQL · Redis · React