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
- The server serves the React app and exposes REST (
/rest/*) and GraphQL (/graphql,/metadata) endpoints. - Mutations and queries run through TypeORM against PostgreSQL, scoped to the caller's workspace and role.
- Long-running work (email/calendar import, workflow execution, AI agent steps, cleanup) is enqueued on BullMQ (backed by Redis) and executed by the worker.
- 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/contributedocs.
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_KEYmeans 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 dedicatedENCRYPTION_KEYbefore 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 ensureX-Forwarded-ForandX-Forwarded-Protoheaders are set.SERVER_URLmust 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, andIS_MULTIWORKSPACE_ENABLED. These can only be set in.envand 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/7days), 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_ENABLEDis env-only and requires a restart; it cannot be toggled from the admin panel.
5.4 Google Workspace integration
- In Google Cloud Console, create a project and enable the Gmail API, Google Calendar API, and People API.
- 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)
- 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).
- Create a project in Microsoft Azure and obtain credentials.
- Grant Microsoft Graph permissions:
Mail.ReadWrite,Mail.Send(only needed to send mail from workflow actions —Mail.Readsuffices for receive-only),Calendars.Read,User.Read,openid,email,profile,offline_access. - Add redirect URIs:
https://{your-domain}/auth/microsoft/redirect(SSO) andhttps://{your-domain}/auth/microsoft-apis/get-access-token. - 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=LOCALorCODE_INTERPRETER_TYPE=LOCALin production with untrusted code — the code executes directly on the host with no isolation. UseLAMBDA/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_PROXYcontrols 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_DRIVERandOTLP_COLLECTOR_METRICS_ENDPOINT_URL(OpenTelemetry collector config ships undertwenty-docker/otel-collectorandtwenty-docker/grafana). HEALTH_METRICS_TIME_WINDOW_IN_MINUTES(5) controls the health metrics window.
8. Operations Checklist
- [ ]
.envcreated from.env.example - [ ]
ENCRYPTION_KEYgenerated withopenssl rand -base64 32and stored in a secrets manager - [ ]
PG_DATABASE_PASSWORDset to a strong value (no special characters) - [ ]
SERVER_URLmatches the external browser URL - [ ] Reverse proxy with SSL termination configured;
X-Forwarded-*headers passed - [ ]
STORAGE_TYPE=S_3configured for production uploads - [ ]
EMAIL_DRIVER=smtpconfigured (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
DISABLEDor 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_KEYbefore 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+):
- Generate a new key:
openssl rand -base64 32. - 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 - Re-encrypt existing rows:
docker exec -it {server_container} yarn command:prod secret-encryption:rotateFlag Description -s, --site <site>Limit to one site -b, --batch-size <n>Rows per batch (default 200, max5000)-d, --dry-runDecrypt + re-encrypt in memory, skip the UPDATE - When a
--dry-runshows zero remaining rows, removeFALLBACK_ENCRYPTION_KEYand 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_DAYSto issue a new current key once the existing one exceeds that age (via thecron:rotate-signing-keysjob). 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 thatkid.
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_secretsecurely — 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
- Redirect the user to
/oauth/authorizewithclient_id,response_type=code,redirect_uri,scope, a randomstate, and a PKCEcode_challenge(code_challenge_method=S256). - Handle the callback at your
redirect_uri?code=...&state=...; verifystate. - Exchange the code at
/oauth/tokenwithgrant_type=authorization_code, thecode,redirect_uri, client credentials, andcode_verifier. You receiveaccess_token,refresh_token, andexpires_in(3600 s). - Call the API with
Authorization: Bearer ACCESS_TOKEN. - 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 --privateuploads to a server. Sharing a private app across workspaces is an Enterprise feature. Version inpackage.jsonmust be strictly higher than the deployed version (VERSION_ALREADY_EXISTSotherwise). - npm marketplace: add the
twenty-appkeyword topackage.jsonand runyarn twenty app:publish. The server syncs the npm catalog hourly (or triggerdev:catalog-sync). - Server compatibility: declare
engines.twenty(a semver range, e.g.>=2.3.0) inpackage.json; the server rejects installs that don't satisfy it (SERVER_VERSION_INCOMPATIBLE).MAX_TARBALL_UPLOAD_SIZE_BYTEScaps 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 --volumeson 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:reseterases 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 |