# Twenty Admin Guide

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`:

```text
                         ┌─────────────────────────────────┐
        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
bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
```

To pin a version or branch:

```bash
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.**

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

The example `.env`:

```ini
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.**

```bash
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.**

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

**Step 5 — Launch.**

```bash
docker compose up -d
```

**Step 6 — Access.** Visit [http://localhost:3000](http://localhost:3000), or on a server:

```bash
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)"

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

=== "Reverse proxy + SSL (standard ports)"

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

=== "Reverse proxy + custom port"

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

Restart after changing it:

```bash
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:

```text
*.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](https://console.cloud.google.com/), 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:

```ini
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](https://portal.azure.com/) 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"

    ```ini
    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"

    ```ini
    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)"

    ```bash
    docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev
    ```
    ```ini
    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:

```ini
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:

```ini
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

```bash
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:

```bash
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:**

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

**Automated daily backup (crontab):**

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

**Full-cluster dump (recommended before upgrades):**

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

**Restore:**

```bash
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

```bash
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

```bash
docker exec -it {server_container} yarn command:prod upgrade:status
```

```text
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:
   ```ini
   ENCRYPTION_KEY=NEW_VALUE
   FALLBACK_ENCRYPTION_KEY=OLD_VALUE
   ```
3. Re-encrypt existing rows:
   ```bash
   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}/` |

```text
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](https://datatracker.ietf.org/doc/html/rfc7591) — 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

```bash
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

```bash
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` |

```json
{
  "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:

```javascript
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>`):

```bash
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)

```text
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):

```bash
# 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:

```sql
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

```bash
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 |

---

_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. Unofficial; not affiliated with or reviewed by the project._
