> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Twenty

> Deploy Twenty, the open-source CRM with customizable objects, pipelines and views, a background worker, a bundled single-node Redis and PostgreSQL that is single-instance by default or highly available with one flag. Needs an existing GVC and an opaque secret holding the app key.

## Overview

Twenty is an open-source CRM — a self-hosted alternative to Salesforce, HubSpot and Pipedrive — with customizable objects, pipelines, views and workflows behind a REST and GraphQL API. This template deploys the Twenty server (React front end plus NestJS API on port `3000`), a background worker that drains its job queues and runs its cron jobs, a bundled single-node Redis, and a PostgreSQL database that is a single instance by default or a highly available Patroni cluster with one flag. Attachments go to a shared persistent volume or to an S3 bucket you own. Twenty is AGPL-3.0 licensed; the template runs the unmodified upstream image.

<Note>
  This template does not create a GVC. It deploys into an existing [GVC](/reference/gvc) that you already have; pass it with `--gvc GVC_NAME`.
</Note>

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-twenty` | Twenty server — UI, REST and GraphQL API (stateful, `twenty.replicas` replicas, port `3000`) | — |
| Workload | `RELEASE_NAME-twenty-worker` | Background worker — job queues and cron jobs (standard, fixed at 1 replica, no inbound access) | — |
| Workload | `RELEASE_NAME-twenty-redis` | Bundled Redis — job queues and cache (stateful, fixed at 1 replica, port `6379`) | — |
| Workload | `RELEASE_NAME-postgres` | Single-instance PostgreSQL 18 (stateful) | `postgres.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres-backup` | Scheduled logical database backup (cron) | `postgres.backup.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha` | Patroni PostgreSQL 17 cluster (stateful, `postgresHA.replicas` replicas) | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-etcd` | etcd consensus store for Patroni (stateful) | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-proxy` | HAProxy leader endpoint Twenty connects to (standard) | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-backup` | Scheduled logical database backup (cron) | `postgresHA.backup.enabled: true` with `mode: logical` |
| Volume Set | `RELEASE_NAME-twenty-storage` | Shared (read-write-many) attachment storage mounted by the server and the worker at `/app/packages/twenty-server/.local-storage` | `storage.type: local` (default) |
| Volume Set | `RELEASE_NAME-twenty-redis-vs` | Redis append-only file at `/data` | — |
| Volume Set | `RELEASE_NAME-pg-vs` | Single-instance database data | `postgres.enabled: true` (default) |
| Volume Set | `RELEASE_NAME-postgres-ha-vs`, `RELEASE_NAME-etcd-vs` | Database and etcd data, one volume per replica | `postgresHA.enabled: true` |
| Secret | `my-twenty-db-credentials` | Dictionary secret with the database `username`, `password` and `database`, built by this template from `postgres.credentials.*`; the name comes from `postgres.config.credentialsSecretName` or `postgresHA.config.credentialsSecretName` | — |
| Secret | `RELEASE_NAME-twenty-creds` | Dictionary secret holding the bundled Redis password | — |
| Secret | `RELEASE_NAME-twenty-server-start`, `RELEASE_NAME-twenty-worker-start` | Start scripts for the server and worker containers | — |
| Secret | `RELEASE_NAME-etcd-startup`, `RELEASE_NAME-patroni-startup`, `RELEASE_NAME-postgres-proxy-startup`, `RELEASE_NAME-wal-g-backup-script` | Start scripts of the highly available database | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-postgres-config` | Non-sensitive backup settings of the highly available database | `postgresHA.backup.enabled: true` |
| Identity | `RELEASE_NAME-twenty-identity` | Shared by the server, worker and Redis workloads; carries the AWS cloud account link in keyless S3 mode | — |
| Policy | `RELEASE_NAME-twenty-policy` | Grants the identity `reveal` on exactly the secrets Twenty reads, including your prerequisite secrets | — |
| Identity, Policy | `RELEASE_NAME-pg-identity`, `RELEASE_NAME-pg-policy` | Single-instance database identity and its secret access | `postgres.enabled: true` (default) |
| Identity, Policy | `RELEASE_NAME-postgres-ha-identity`, `RELEASE_NAME-postgres-ha-policy`, `RELEASE_NAME-etcd-identity`, `RELEASE_NAME-etcd-policy` | Database and etcd identities and their secret access | `postgresHA.enabled: true` |

Your app key secret (see [Prerequisites](#prerequisites)) is **not** created by the template.

## Prerequisites

Twenty encrypts stored OAuth tokens, TOTP secrets and app variables, and signs its auth tokens, with one app key that you supply through an [opaque secret](/guides/create-secret/opaque) created **before** installing. The value never passes through Helm values; the template hands it to Twenty as both `APP_SECRET` and `ENCRYPTION_KEY`.

<Steps>
  <Step title="Create the app key secret">
    The payload is a single random string. Secrets are org-level, so no `--gvc` flag is involved:

    ```bash theme={null}
    printf '%s' "$(openssl rand -hex 32)" | cpln secret create-opaque --name APP_KEY_SECRET_NAME --encoding plain -f -
    ```

    Set `secrets.name` to this name.
  </Step>

  <Step title="Back the value up">
    Store a copy outside Control Plane. The key is effectively write-once: changing `secrets.name` without pointing `secrets.fallbackName` at the previous key makes stored OAuth tokens, TOTP secrets and app variables undecryptable and logs everyone out. See [Rotating the App Key](#rotating-the-app-key).
  </Step>

  <Step title="Choose the two bundled passwords">
    The database password (`postgres.credentials.password`) and the Redis password (`redis.auth.password`) ship as `change-me-…` placeholders that the template uses as-is. Pick strong values and pass them at install time. Both are embedded in connection URLs, so use only letters, digits, `-` and `_`; the chart refuses to render anything else. The database password seeds the database on first boot and is not changed by later value edits.
  </Step>
</Steps>

<Warning>
  **If the app key secret does not exist at install time, the deployment wedges silently.** `cpln logs` returns zero lines — the container never starts, so it has nothing to log. The one place the reason appears is `status.versions[].message`:

  ```bash theme={null}
  cpln workload get-deployments RELEASE_NAME-twenty --gvc GVC_NAME -o yaml
  ```

  Note this is `get-deployments` — plain `cpln workload get` has no `versions` field. Creating the secret repairs the deployment on its own after several minutes, or force a redeployment to skip the wait.
</Warning>

Everything else works with the defaults. Three optional features need their own setup first:

* **S3 attachment storage** — an existing bucket plus either a Control Plane cloud account (AWS S3) or a static-key dictionary secret (S3-compatible servers). See [Attachment Storage](#attachment-storage). Required for more than one server replica.
* **Database backups** — a bucket and access setup for AWS S3, Google Cloud Storage or an S3-compatible server. See [Backing Up](#backing-up).
* **App key rotation** — a second opaque secret holding the previous key, referenced by `secrets.fallbackName` for the duration of the rotation. See [Rotating the App Key](#rotating-the-app-key).

## Installation

Once the app key secret exists, install into your existing GVC with the two bundled passwords set:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/twenty \
  --version 1.3.0 --gvc GVC_NAME \
  --set secrets.name=APP_KEY_SECRET_NAME \
  --set postgres.credentials.password='YOUR-STRONG-DB-PASSWORD' \
  --set redis.auth.password='YOUR-STRONG-REDIS-PASSWORD'
```

To install using another method, follow the instructions for it:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

Each subsection shows the shipped defaults for one top-level values area.

### Twenty Server

```yaml theme={null}
twenty:
  # One image serves the React front end and the NestJS API on :3000.
  # Docker tag keeps the leading "v"; Chart appVersion drops it.
  image: twentycrm/twenty:v2.26.1
  # Stateless web tier. >1 REQUIRES storage.type: s3 and moves boot migrations
  # to the worker (they must run in exactly one container).
  replicas: 1
  # Public base URL. MUST match the URL browsers use or auth/CORS fail with
  # opaque errors. Empty = the platform canonical endpoint. Set WITH https://
  # only for a custom domain.
  serverUrl: ""
  # Postgres connections per pool, per process. Raise replicas and this together
  # with care — the database has a finite connection limit.
  dbPoolMaxConnections: 10
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi   # headroom for first-boot migrations and upgrade backfills
```

Both the server and the worker run `twenty.image`. Leave `twenty.serverUrl` empty on a public install: the server then advertises its own canonical endpoint, and the worker advertises the server's — never its own. Set it, with the scheme, whenever browsers open Twenty at any other address: a custom domain (`https://crm.example.com`), or the address you reach through a port-forward on a private install (`http://localhost:3000`). Twenty compares the browser origin against `SERVER_URL`, so a mismatch breaks sign-in and CORS with opaque errors.

### Background Worker

```yaml theme={null}
# Same image, `yarn worker:prod`. Always exactly one replica: it owns cron
# registration, and boot migrations when twenty.replicas > 1.
worker:
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 2Gi
```

The worker drains the job queues (entity events, webhooks, workflow triggers, file maintenance) and runs the registered cron jobs. It is a fixed single replica with no `worker.replicas` knob, exposes no port and accepts no inbound traffic. It has no probes, so it reports `ready` as soon as it is scheduled — read its logs rather than its readiness:

```bash theme={null}
cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-twenty-worker"}' --limit 50 --since 10m
```

### App Key Secret

```yaml theme={null}
secrets:
  name: my-twenty-app-secret
  # Optional: opaque secret holding the PREVIOUS key, only during a rotation
  # (e.g. my-twenty-app-secret-old). Empty = no fallback.
  fallbackName: ""
```

`secrets.name` is the prerequisite opaque secret from [Prerequisites](#prerequisites). `secrets.fallbackName` is set only while rotating the key — see [Rotating the App Key](#rotating-the-app-key).

### Attachment Storage

```yaml theme={null}
storage:
  # local uses a SHARED (read-write-many) volumeset mounted by both the server
  # and the worker, so file-maintenance jobs see uploads. Shared volumes support
  # expand only — no snapshots — and live in one location, so s3 is still the
  # durable choice for production and is required for twenty.replicas > 1.
  type: local                 # local | s3
  local:
    volumeset:
      capacity: 10            # GiB (minimum 10); mounted at /app/packages/twenty-server/.local-storage
  s3:
    bucket: my-twenty-bucket
    region: us-east-1
    endpoint: ""              # set for S3-compatible servers (e.g. http://my-minio:9000)
    # Keyless auth (AWS, preferred): Control Plane cloud account + your
    # pre-created IAM policy scoped to the bucket (JSON below).
    # Used only when auth.secretName is empty.
    cloudAccountName: my-s3-cloud-account
    policyName: my-twenty-s3-policy
    auth:
      # Static-key dictionary secret (STORAGE_S3_ACCESS_KEY_ID +
      # STORAGE_S3_SECRET_ACCESS_KEY) — ONLY for S3-compatible servers
      # (requires endpoint set). AWS S3 is always keyless via the cloud account.
      secretName: ""
```

<Tabs>
  <Tab title="Shared volume (default)">
    Attachments are written to a **shared (read-write-many) volume set** mounted at `/app/packages/twenty-server/.local-storage` by both the server and the worker, so the worker's file-maintenance jobs see what the server uploaded. Files survive restarts, redeploys and upgrades under the same release name.

    <Warning>
      Shared volume sets support **expansion only — they cannot be snapshotted** — and exist in a single location. For production, `storage.type: s3` is the durable choice, and it is required for `twenty.replicas` above `1`; the chart refuses to render that combination with local storage.
    </Warning>
  </Tab>

  <Tab title="AWS S3 (keyless)">
    AWS S3 uses a Control Plane cloud account: no access keys exist anywhere, and the Twenty identity authenticates through the account at runtime. This is the only supported way to reach AWS S3 — the chart rejects static keys unless `storage.s3.endpoint` is set.

    <Steps>
      <Step title="Create a bucket">
        Create an S3 bucket. Set `storage.s3.bucket` and `storage.s3.region` to match.
      </Step>

      <Step title="Set up a cloud account">
        If you do not have one, [create a cloud account](/guides/create-cloud-account) for your AWS account. Set `storage.s3.cloudAccountName` to its name.
      </Step>

      <Step title="Create a bucket-scoped IAM policy">
        Create an AWS IAM policy with the JSON below (replace `YOUR_BUCKET_NAME`), then set `storage.s3.policyName` to the policy's name:

        ```json theme={null}
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket"
              ],
              "Resource": [
                "arn:aws:s3:::YOUR_BUCKET_NAME",
                "arn:aws:s3:::YOUR_BUCKET_NAME/*"
              ]
            }
          ]
        }
        ```
      </Step>

      <Step title="Switch the storage type">
        Set `storage.type: s3` and leave `storage.s3.auth.secretName` empty. The template attaches the cloud account and your policy to the Twenty identity.
      </Step>
    </Steps>

    Twenty writes its app-registration assets into the bucket at boot, so expect a few objects there before anyone uploads an attachment.
  </Tab>

  <Tab title="S3-compatible (MinIO and others)">
    S3-compatible servers use static access keys held in a dictionary secret you create before installing.

    <Steps>
      <Step title="Create a bucket">
        Create the bucket on your server. For the [MinIO](/template-catalog/templates/minio) template deployed in the same GVC, the S3 API address is `http://RELEASE_NAME-minio.GVC_NAME.cpln.local:9000`.
      </Step>

      <Step title="Set the endpoint">
        Set `storage.type: s3`, `storage.s3.endpoint` to the S3 API address including scheme and port, and `storage.s3.bucket` to the bucket name.
      </Step>

      <Step title="Create the key secret">
        Create a dictionary secret with the server's credentials and set `storage.s3.auth.secretName` to its name. The Twenty identity is granted `reveal` on exactly this secret.

        ```bash theme={null}
        cpln secret create-dictionary --name STORAGE_KEYS_SECRET_NAME \
          --entry STORAGE_S3_ACCESS_KEY_ID=YOUR_ACCESS_KEY \
          --entry STORAGE_S3_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

### Access

```yaml theme={null}
publicAccess:
  enabled: true               # HTTPS UI + API on the auto *.cpln.app endpoint; false = internal-only
internalAccess:
  type: same-gvc              # none | same-gvc | same-org | workload-list
  workloads: []               # used only with workload-list
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

Public access is on by default. Set `publicAccess.enabled: false` for an internal-only instance — external requests are then refused, and in-GVC callers still reach it according to `internalAccess.type`:

| Type | Description |
| - | - |
| `none` | No internal access. |
| `same-gvc` | Allow access from all workloads in the same GVC (default). |
| `same-org` | Allow access from all workloads in the same org. |
| `workload-list` | Allow access only from the workloads listed in `internalAccess.workloads`. |

The worker and the bundled Redis are never publicly reachable: the worker accepts no inbound traffic, and Redis accepts only same-GVC traffic. A change to either access knob takes a few minutes to take effect.

### Redis

```yaml theme={null}
redis:
  image: redis:8.10.0
  auth:
    password: change-me-twenty-redis   # template-managed; wired into REDIS_URL — letters/digits/-/_ only
  resources:
    minCpu: 100m
    maxCpu: 400m
    minMemory: 256Mi
    maxMemory: 512Mi
  volumeset:
    capacity: 10              # GiB (minimum 10); AOF at /data
```

Redis is required — Twenty does not boot without `REDIS_URL` — and it runs as a single node with persistence on and eviction disabled, so a queued job is never dropped. Twenty accepts only a plain `redis://` URL, which is why the Sentinel-based [redis](/template-catalog/templates/redis) template is not used here. Change `redis.auth.password` before installing; only letters, digits, `-` and `_` are accepted because the value is embedded in a connection URL.

### Database

Enable exactly one of `postgres` (single instance, default) and `postgresHA` (highly available) — the chart refuses to render with both or neither. Twenty is wired to the active database automatically: the single instance directly, or the HAProxy leader endpoint in HA mode. In **both** modes the database credentials come from `postgres.credentials.*`: this template writes them into a dictionary secret named by `postgres.config.credentialsSecretName` or `postgresHA.config.credentialsSecretName` and hands that name to the bundled database. There is nothing for you to create.

```yaml theme={null}
# ─── Database: Single-instance PostgreSQL 18 (default) ────────────
# One Postgres workload — the light default. Disable this and enable postgresHA
# for a redundant database tier.
postgres:
  enabled: true
  image: postgres:18          # PostgreSQL 18 (the HA path below runs 17 — the majors differ)
  # Credentials for the bundled database — internal plumbing no human types elsewhere.
  # Twenty CREATES the dictionary secret named by config.credentialsSecretName below out
  # of these three values, so there is nothing for you to create before installing.
  credentials:
    username: twenty
    password: change-me-twenty-db     # change before install; letters/digits/-/_ only (it is embedded in a URL)
    database: twenty
  config:
    # Name of the dictionary secret this chart creates and the bundled postgres reads.
    # Secret names are org-wide: give each twenty release its own name. A second
    # release left on this name is REFUSED at install (it is owned by the first
    # release) — nothing is shared, overwritten or deleted.
    credentialsSecretName: my-twenty-db-credentials
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 1Gi
  volumeset:
    capacity: 10              # GiB (minimum 10)

  backup:                     # optional database backups — see Backing Up
    enabled: false
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0   # tag must match the postgres major above
    schedule: "0 2 * * *"
    resources:
      cpu: 100m
      memory: 128Mi
    provider: aws             # options: aws, gcp, minio
    aws:
      bucket: my-twenty-pg-backup-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: my-twenty-pg-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-twenty-pg-backup-bucket
      cloudAccountName: my-gcs-cloud-account
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: my-twenty-pg-backup-bucket
      # REQUIRED prerequisite dictionary secret holding `accessKey` and `secretKey`,
      # only when this backup is enabled with provider: minio. See Backing Up.
      credentialsSecretName: my-twenty-minio-credentials
      prefix: postgres/backups

# ─── Database: Highly Available PostgreSQL 17 (opt-in) ────────────
# Deploys the postgres-highly-available template: 3 Patroni replicas, 3 etcd
# replicas, and an HAProxy leader endpoint Twenty connects to. Enable this and
# set postgres.enabled: false.
postgresHA:
  enabled: false
  config:
    # Name of the dictionary secret this chart creates and the bundled
    # postgres-highly-available reads. Since pgHA 2.5.0 the subchart creates no
    # credentials secret of its own, so both stores are fed the same one, built
    # from postgres.credentials.* above.
    credentialsSecretName: my-twenty-db-credentials
  replicas: 3
  resources:
    minCpu: 500m
    maxCpu: 1000m
    minMemory: 1Gi
    maxMemory: 2Gi
  volumeset:
    capacity: 10              # GiB per replica (minimum 10)

  backup:                     # optional database backups — see Backing Up
    enabled: false
    mode: logical             # logical or wal-g
    resources:
      cpu: 100m
      memory: 128Mi
    logical:
      image: ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0
      schedule: "0 2 * * *"
    walg:
      intervalSeconds: 21600
    provider: aws             # options: aws, gcp, minio
    aws:
      bucket: my-twenty-pg-backup-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: my-twenty-pg-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-twenty-pg-backup-bucket
      cloudAccountName: my-gcs-cloud-account
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: my-twenty-pg-backup-bucket
      # REQUIRED PREREQUISITE SECRET when provider is `minio` — a `dictionary`
      # secret holding exactly `accessKey` and `secretKey`. See Backing Up.
      credentialsSecretName: my-twenty-minio-credentials
      prefix: postgres/backups
```

The other keys of the bundled [postgres](/template-catalog/templates/postgres) and [postgres-highly-available](/template-catalog/templates/postgres-highly-available) templates are available under `postgres.*` and `postgresHA.*`, with one exception: `postgresHA.proxy.enabled` must stay `true`, because the HAProxy endpoint is the address Twenty connects to.

<Warning>
  Choose the database mode **before** you have data. The two paths run different PostgreSQL majors (18 and 17) on separate volume sets, so flipping `postgres.enabled` / `postgresHA.enabled` on a live release points Twenty at a different, empty database — your existing records are orphaned, not migrated. Moving between the modes is a dump and restore.
</Warning>

<Warning>
  Secret names are org-wide, and the credentials secret is created by this template in both modes. If you run more than one release of this template in the same org, give each its own `postgres.config.credentialsSecretName` (or `postgresHA.config.credentialsSecretName`). A second release left on the default name is **refused at install** with `cannot be updated because it is being managed by a different release` and creates nothing; the first release is unaffected.
</Warning>

## Connecting

| What | Address | Notes |
| - | - | - |
| Web UI and REST API | `https://<canonical>.cpln.app` | `status.canonicalEndpoint` of `RELEASE_NAME-twenty` — see the command below |
| GraphQL — workspace records | `POST https://<canonical>.cpln.app/graphql` | Authenticated with an API key or a user session |
| GraphQL — auth and core | `POST https://<canonical>.cpln.app/metadata` | Same authentication |
| Health check | `GET https://<canonical>.cpln.app/healthz` | Unauthenticated; the readiness and liveness probe |
| Internal (same GVC) | `http://RELEASE_NAME-twenty.GVC_NAME.cpln.local:3000` | Subject to `internalAccess.type` |
| Login | The first account created after install | See [First Run](#first-run) |
| PostgreSQL (single-instance mode) | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432` | Credentials in the secret named by `postgres.config.credentialsSecretName` |
| PostgreSQL (HA mode) | `RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local:5432` | Credentials in the secret named by `postgresHA.config.credentialsSecretName` |
| Redis | `RELEASE_NAME-twenty-redis.GVC_NAME.cpln.local:6379` | Password in the `redisPassword` key of `RELEASE_NAME-twenty-creds` |

Read the public hostname and the stored credentials:

```bash theme={null}
cpln workload get RELEASE_NAME-twenty --gvc GVC_NAME -o yaml        # status.canonicalEndpoint
cpln secret reveal DB_CREDENTIALS_SECRET_NAME -o yaml               # the name you set in *.config.credentialsSecretName
cpln secret reveal RELEASE_NAME-twenty-creds -o yaml                # redisPassword
```

To verify the server from your machine without going through the public endpoint, forward port `3000` and query the health route:

```bash theme={null}
cpln port-forward RELEASE_NAME-twenty 3000:3000 --gvc GVC_NAME
curl -s http://localhost:3000/healthz
```

On an install with `publicAccess.enabled: false`, the same port-forward is how you reach the UI in a browser at `http://localhost:3000` — and `twenty.serverUrl` must then be set to that address, because Twenty checks the browser origin against it.

### First Run

Twenty ships no default account. The first person to sign up creates the workspace and becomes its full administrator.

<Steps>
  <Step title="Wait for the server workload to report ready">
    A first install takes several minutes: the database and Redis come up first, then the server waits for a live PostgreSQL, runs its boot migrations and only then starts listening on port `3000`. Every fresh install logs `relation "core.appToken" does not exist` and similar lines early in the boot — the migration framework probing for tables it has not created yet — and the readiness probe may report a transient failure during the migration window. Both stop on their own once migrations finish; do not abort the install.

    ```bash theme={null}
    cpln workload get-deployments RELEASE_NAME-twenty --gvc GVC_NAME -o yaml
    ```
  </Step>

  <Step title="Sign up immediately">
    Browse to the canonical endpoint of `RELEASE_NAME-twenty` and create the first account. On a public endpoint, whoever reaches the URL first owns the CRM — sign up as soon as the workload is ready, or install with `publicAccess.enabled: false` and reach the sign-up page through a port-forward (set `twenty.serverUrl` to `http://localhost:3000` for that session).
  </Step>

  <Step title="Finish configuration inside the app">
    SMTP, AI provider keys, rate limits and OAuth/SSO are not template values. Set them in **Settings → Admin Panel → Configuration Variables**; they are stored in the database and apply without a redeploy.
  </Step>
</Steps>

## Operations

### Backing Up

Database backups are optional and off by default. They cover the PostgreSQL database — every record, workspace and user. Attachments are not included: in S3 mode they live in your bucket, in local mode on the `RELEASE_NAME-twenty-storage` volume set, which cannot be snapshotted.

Enable backups with `postgres.backup.enabled: true` or `postgresHA.backup.enabled: true` (matching your database mode) and complete the storage setup for your provider **before** installing. The keys below are shown as `backup.*`; set them inside the enabled database block.

<Tabs>
  <Tab title="AWS S3">
    <Steps>
      <Step title="Create a bucket">
        Create an S3 bucket. Set `backup.aws.bucket` and `backup.aws.region` to match.
      </Step>

      <Step title="Set up a cloud account">
        If you do not have one, [create a cloud account](/guides/create-cloud-account) for your AWS account. Set `backup.aws.cloudAccountName` to its name.
      </Step>

      <Step title="Create a bucket-scoped IAM policy">
        Create an AWS IAM policy with the same bucket-scoped JSON shown under [Attachment Storage](#attachment-storage) (replace `YOUR_BUCKET_NAME` with the backup bucket), then set `backup.aws.policyName` to the policy's name.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Google Cloud Storage">
    <Steps>
      <Step title="Create a bucket">
        Create a GCS bucket. Set `backup.gcp.bucket` to its name.
      </Step>

      <Step title="Set up a cloud account">
        If you do not have one, [create a cloud account](/guides/create-cloud-account) for your GCP project. Set `backup.gcp.cloudAccountName` to its name — access is keyless.
      </Step>

      <Step title="Grant bucket access">
        Grant the `Storage Object Admin` role (`roles/storage.objectAdmin`) on that bucket — not on the whole project — to the service account created for the cloud account.
      </Step>
    </Steps>
  </Tab>

  <Tab title="S3-compatible (MinIO and others)">
    <Steps>
      <Step title="Create a bucket">
        Create your bucket on the server. Set `backup.minio.bucket` to its name.
      </Step>

      <Step title="Set the endpoint">
        Set `backup.minio.endpoint` to the S3 API address including scheme and port. For the [MinIO](/template-catalog/templates/minio) template deployed in the same GVC, this is `http://RELEASE_NAME-minio.GVC_NAME.cpln.local:9000`. No cloud account is involved.
      </Step>

      <Step title="Create the credentials secret">
        In both database modes the server's keys are a prerequisite [dictionary secret](/guides/create-secret/dictionary) holding exactly `accessKey` and `secretKey`. Create it before installing and set `backup.minio.credentialsSecretName` to its name:

        ```bash theme={null}
        cpln secret create-dictionary --name MINIO_BACKUP_SECRET_NAME \
          --entry accessKey=YOUR_ACCESS_KEY \
          --entry secretKey=YOUR_SECRET_KEY
        ```
      </Step>
    </Steps>
  </Tab>
</Tabs>

Single-instance mode takes scheduled logical dumps through the `RELEASE_NAME-postgres-backup` cron workload on `backup.schedule`. In HA mode, `backup.mode` selects `logical` (a scheduled dump run by `RELEASE_NAME-postgres-ha-backup`) or `wal-g` (continuous WAL archiving every `backup.walg.intervalSeconds`). `backup.*.prefix` is the key prefix within the bucket; schedules are cron expressions in UTC.

### Restoring a Backup

The database restore is the one documented for the bundled database: [Restoring a Backup](/template-catalog/templates/postgres#restoring-a-backup) on the postgres page, or [Restoring a Backup](/template-catalog/templates/postgres-highly-available#restoring-a-backup) on the postgres-highly-available page (logical and WAL-G). In HA mode, connect through `RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local` so the restore targets the current leader; use the username and password from the secret named by `*.config.credentialsSecretName`.

Three Twenty-specific points:

* The server and worker must not be writing to the database while a dump is replayed. After the restore, force a redeployment of both so they reconnect:

  ```bash theme={null}
  cpln workload force-redeployment RELEASE_NAME-twenty --gvc GVC_NAME
  cpln workload force-redeployment RELEASE_NAME-twenty-worker --gvc GVC_NAME
  ```

* Attachments are restored separately: copy them back into the bucket (S3 mode) or onto the `RELEASE_NAME-twenty-storage` volume set (local mode).

* Keep the app key from your prerequisite secret with the backup. A dump restored under a different `ENCRYPTION_KEY` cannot decrypt the OAuth tokens, TOTP secrets and app variables it contains.

<Warning>
  This restore path has **not** been exercised against a Twenty install. The database procedure is the bundled template's own; rehearse it on a test release before you depend on it.
</Warning>

### Rotating the App Key

`secrets.name` points at the opaque secret holding the current app key. `secrets.fallbackName` points at a second opaque secret holding the **previous** key and exists only for a rotation: Twenty uses it as `FALLBACK_ENCRYPTION_KEY`, a verification-only key that keeps rows encrypted under the old key readable while new writes use the new key.

<Steps>
  <Step title="Create the new key secret">
    ```bash theme={null}
    printf '%s' "$(openssl rand -hex 32)" | cpln secret create-opaque --name NEW_APP_KEY_SECRET_NAME --encoding plain -f -
    ```
  </Step>

  <Step title="Upgrade with both names set">
    Run `helm upgrade` with `secrets.name` pointing at the new secret and `secrets.fallbackName` at the old one. The identity is granted `reveal` on exactly those two secrets plus the ones the template creates.

    ```yaml theme={null}
    secrets:
      name: NEW_APP_KEY_SECRET_NAME
      fallbackName: APP_KEY_SECRET_NAME
    ```
  </Step>

  <Step title="Let the rollout finish">
    The server re-runs its boot migrations during this rollout, so it takes about as long as a first install. Sessions issued under the old key keep working and existing records stay readable. Twenty's documentation does not say that rows are re-encrypted under the new key, so do not clear `secrets.fallbackName` on an assumption — keep both secrets until you have confirmed the old key is no longer needed.
  </Step>
</Steps>

<Warning>
  Never change `secrets.name` without setting `secrets.fallbackName` to the previous key. Stored OAuth tokens, TOTP secrets and app variables are encrypted with the old value and become undecryptable, and every user is logged out.
</Warning>

### Upgrading From 1.0.0

Version `1.0.1` only updates the bundled highly available database to a release that compacts its etcd cluster. Without compaction, etcd's backend grows with time alone and eventually goes read-only, taking PostgreSQL failover with it. Only installs running the HA database (`postgresHA.enabled: true`, off by default here) are affected — see [etcd History Compaction](/template-catalog/templates/postgres-highly-available#etcd-history-compaction) for the mechanism and the symptoms. Compaction stops further growth but cannot shrink a backend that has already grown.

### Upgrading From 1.0.1 or Earlier

Version `1.1.0` changed the single-instance database path:

* `postgres.config.username`, `postgres.config.password` and `postgres.config.database` moved to `postgres.credentials.username`, `postgres.credentials.password` and `postgres.credentials.database`. The new `postgres.config.credentialsSecretName` names the dictionary secret this template now creates from those three values. Carrying the old keys fails the render with `config.username was REMOVED in postgres 3.4.0` — move the three keys and you are done, and ignore that message's advice to create a secret yourself.
* `postgres.backup.minio.accessKey` and `postgres.backup.minio.secretKey` were replaced by `postgres.backup.minio.credentialsSecretName`, a prerequisite dictionary secret holding `accessKey` and `secretKey` — see [Backing Up](#backing-up).

Keep `postgres.credentials.*` equal to the values your database was initialised with; the running database still enforces them.

### Upgrading From 1.1.0 or Earlier

Version `1.2.0` moved the highly available database to `postgres-highly-available` 2.5.0, which no longer creates its own credentials secret:

* `postgresHA.postgres.username`, `postgresHA.postgres.password` and `postgresHA.postgres.database` were removed. Carrying them fails the render with `the postgres block was REMOVED in 2.5.0`. Delete the block.
* In HA mode the credentials now come from `postgres.credentials.*` — the same keys the single-instance path uses — and this template writes them into the secret named by the new `postgresHA.config.credentialsSecretName`.

<Warning>
  **Use the credentials your cluster already has.** Set `postgres.credentials.username`, `postgres.credentials.password` and `postgres.credentials.database` to exactly the values your `postgresHA.postgres.*` block held. The database was initialised with them and still enforces them; putting new values in the secret does not change the running database — it gives Twenty a password that no longer works.
</Warning>

### Upgrading From 1.2.0 or Earlier

Version `1.2.1` replaced `postgresHA.backup.minio.accessKey` and `postgresHA.backup.minio.secretKey` with `postgresHA.backup.minio.credentialsSecretName`, a prerequisite dictionary secret holding `accessKey` and `secretKey`. Only installs backing up the HA database to an S3-compatible server are affected — see [Backing Up](#backing-up) for the secret.

Version `1.3.0` changes no values. It makes the server and worker wait for a live PostgreSQL before running boot migrations, which fixes a first-install failure where the containers could start against a database that was not yet accepting connections.

### Scaling and Availability

* **Server tier** — `twenty.replicas` sets how many server replicas run behind the endpoint. Anything above `1` requires `storage.type: s3`; the chart refuses to render the combination with local storage.

  ```yaml theme={null}
  twenty:
    replicas: 2
  storage:
    type: s3
    s3:
      bucket: my-twenty-bucket
      region: us-east-1
      cloudAccountName: my-s3-cloud-account
      policyName: my-twenty-s3-policy
  ```

* **Boot migrations run in exactly one container**, and the template places them automatically:

  | `twenty.replicas` | Who migrates | What the others do |
  | - | - | - |
  | `1` (default) | The server | The worker skips migrations and cron registration |
  | `2` or more | The single worker | Every server replica skips both |

  Because the worker owns migrations above one replica, a **fresh** multi-replica install serves errors until the worker has created the schema. Upgrades of an existing release already have a populated schema.

* **Worker and Redis** — both are fixed at one replica by design: the worker owns cron registration, and Twenty accepts only a plain `redis://` URL. A Redis restart stalls background jobs briefly; the UI stays up.

* **Database** — the opt-in `postgresHA` path keeps serving through a replica failure and a rolling restart; the default `postgres` path has no failover. The mode is an install-time choice.

* **Helm upgrades** — the first `helm upgrade` after an install can restart the bundled database and Redis even when their values did not change; upgrade in a quiet window.

### Troubleshooting

<AccordionGroup>
  <Accordion title="The deployment never starts and cpln logs is empty">
    **Symptom:** `RELEASE_NAME-twenty` and `RELEASE_NAME-twenty-worker` never reach ready, and `cpln logs` returns zero lines.

    **Cause:** a secret referenced by name does not exist — usually the app key secret (`secrets.name`), or the fallback or S3 key secret when those are set. The container never starts, so there is nothing to log.

    **Fix:** read `status.versions[].message`, which names the missing secret, then create it. The deployment recovers on its own after several minutes, or force a redeployment to skip the wait.

    ```bash theme={null}
    cpln workload get-deployments RELEASE_NAME-twenty --gvc GVC_NAME -o yaml
    cpln workload force-redeployment RELEASE_NAME-twenty --gvc GVC_NAME
    ```
  </Accordion>

  <Accordion title="First boot logs relation core.appToken does not exist">
    **Symptom:** a fresh install logs `error: relation "core.appToken" does not exist` and similar lines, and the readiness probe reports a transient failure.

    **Cause:** the migration framework probes for its state tables before creating them. This is expected on every fresh install and stops once the boot migrations finish.

    **Fix:** none — wait for the server to report ready. If the server never does, read its log for a migration error:

    ```bash theme={null}
    cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-twenty"} |= "ERROR"' --limit 50 --since 10m
    ```
  </Accordion>

  <Accordion title="The server logs waiting for Postgres and then exits for restart">
    **Symptom:** the server or worker log repeats `twenty: waiting for Postgres... (attempt N)` and ends with `twenty: Postgres not ready after 250s; exiting for restart`.

    **Cause:** the start script waits for the database to answer a real PostgreSQL handshake before running migrations, and the database was not accepting connections within its window. On a first HA install this can happen once while the cluster elects a leader; a database that never comes up is a different problem.

    **Fix:** the container restarts and retries on its own. If it keeps happening, check the database workload — `RELEASE_NAME-postgres` or `RELEASE_NAME-postgres-ha` — with `cpln workload get-deployments` and its logs.
  </Accordion>

  <Accordion title="Sign-in fails or the browser reports CORS errors">
    **Symptom:** the UI loads but sign-in loops or fails, and the browser console shows CORS or origin errors.

    **Cause:** the address in the browser does not match `SERVER_URL`. This happens with a custom domain, or when reaching a private install through a port-forward, while `twenty.serverUrl` is empty.

    **Fix:** set `twenty.serverUrl` to exactly the URL browsers use, including the scheme (`https://crm.example.com`, or `http://localhost:3000` for a port-forward), and run `helm upgrade`.
  </Accordion>

  <Accordion title="Render fails because replicas above 1 require s3 storage">
    **Symptom:** `helm install` or `helm upgrade` fails before anything is applied, citing `twenty.replicas > 1 requires storage.type: s3`.

    **Cause:** local attachments live on a volume set, which does not serve more than one server replica.

    **Fix:** complete the S3 setup under [Attachment Storage](#attachment-storage) and set `storage.type: s3`, or keep `twenty.replicas: 1`.
  </Accordion>

  <Accordion title="Render fails because static keys are only for S3-compatible servers">
    **Symptom:** the render fails citing `static keys (storage.s3.auth.secretName) are only for S3-compatible servers`.

    **Cause:** `storage.s3.auth.secretName` is set but `storage.s3.endpoint` is empty. AWS S3 is keyless only.

    **Fix:** for AWS S3, clear `storage.s3.auth.secretName` and use `storage.s3.cloudAccountName` plus `storage.s3.policyName`; for an S3-compatible server, set `storage.s3.endpoint`.
  </Accordion>

  <Accordion title="Render fails with config.username was REMOVED in postgres 3.4.0">
    **Symptom:** `helm upgrade` fails before anything is applied, citing `config.username was REMOVED in postgres 3.4.0`.

    **Cause:** your values still carry the `1.0.x` single-instance keys `postgres.config.username`, `postgres.config.password` or `postgres.config.database`.

    **Fix:** move the three keys to `postgres.credentials.*` as described in [Upgrading From 1.0.1 or Earlier](#upgrading-from-1-0-1-or-earlier). Do not create a secret yourself — this template creates it.
  </Accordion>

  <Accordion title="Render fails with the postgres block was REMOVED in 2.5.0">
    **Symptom:** `helm upgrade` fails before anything is applied, citing `the postgres block was REMOVED in 2.5.0`.

    **Cause:** your values still carry `postgresHA.postgres.username`, `postgresHA.postgres.password` or `postgresHA.postgres.database` from version `1.1.0` or earlier.

    **Fix:** delete the `postgresHA.postgres` block and put the same three values under `postgres.credentials.*` — see [Upgrading From 1.1.0 or Earlier](#upgrading-from-1-1-0-or-earlier).
  </Accordion>

  <Accordion title="A second release is refused at install as managed by a different release">
    **Symptom:** installing a second Twenty release in the same org fails with `cannot be updated because it is being managed by a different release`, and nothing is created.

    **Cause:** both releases use the same `credentialsSecretName`. Secret names are org-wide, and the first release owns that secret.

    **Fix:** give the second release its own `postgres.config.credentialsSecretName` (or `postgresHA.config.credentialsSecretName`). The first release is unaffected.
  </Accordion>

  <Accordion title="Render fails because the database password contains other characters">
    **Symptom:** the render fails citing `the database password must contain only letters, digits, '-' and '_'` or the same message for `redis.auth.password`.

    **Cause:** both passwords are embedded in connection URLs, and other characters break the URL.

    **Fix:** choose a password made of letters, digits, `-` and `_` only. For the database password this must be done before the first install — it seeds the database and is not changed by later value edits.
  </Accordion>
</AccordionGroup>

## Important Notes

* **Create the app key secret before installing** — `secrets.name` must point at an existing opaque secret (plain encoding). Without it the deployment wedges silently; see [Prerequisites](#prerequisites).
* **The app key is effectively write-once** — rotate it only with `secrets.fallbackName` set to the previous key, or stored OAuth tokens, TOTP secrets and app variables become undecryptable and everyone is logged out.
* **Sign up immediately after install** — there is no seeded account, and on a public endpoint the first visitor to sign up becomes the workspace administrator. Install with `publicAccess.enabled: false` until you are ready if that is a concern.
* **Set `twenty.serverUrl` whenever browsers open Twenty at anything other than its canonical endpoint** — a custom domain or a port-forward — with the scheme. A mismatch breaks sign-in and CORS with opaque errors.
* **Change `postgres.credentials.password` and `redis.auth.password` before installing** — both are embedded in connection URLs, so use only letters, digits, `-` and `_`; the database password seeds the database on first boot and is not changed by later value edits.
* **Choose the database mode before you have data** — the single-instance and HA paths run PostgreSQL 18 and 17 respectively on separate volume sets; switching is a dump and restore, not a values change.
* **`twenty.replicas` above `1` requires `storage.type: s3`** — the shared local volume set cannot be snapshotted and lives in one location, so S3 is the production recommendation regardless of replica count.
* **AWS S3 is keyless only** — use a cloud account plus a bucket-scoped IAM policy. Static keys are accepted only when `storage.s3.endpoint` points at an S3-compatible server.
* **Give each release its own `credentialsSecretName`** — secret names are org-wide, and a second release on the default name is refused at install.
* **SMTP, AI provider keys, rate limits and OAuth/SSO are not values knobs** — configure them in **Settings → Admin Panel → Configuration Variables** inside the app.
* **Foreign data wrappers ("remote objects") are unavailable** — that feature needs upstream's custom PostgreSQL image, which this template does not deploy.
* **Data survives restarts and upgrades; uninstalling deletes it** — records live in the database volume set and local attachments in the storage volume set, and both go with the release. Your prerequisite app key secret is yours and survives an uninstall.

## External References

<CardGroup cols={2}>
  <Card title="Twenty Documentation" icon="book" href="https://docs.twenty.com/">
    Official Twenty documentation
  </Card>

  <Card title="Self-Hosting Setup" icon="server" href="https://docs.twenty.com/developers/self-host/capabilities/setup">
    Configuration variables and the in-app admin panel
  </Card>

  <Card title="REST and GraphQL API" icon="code" href="https://docs.twenty.com/developers/extend/api">
    Query and mutate CRM records over HTTP
  </Card>

  <Card title="Twenty on GitHub" icon="github" href="https://github.com/twentyhq/twenty">
    Source code and release notes
  </Card>

  <Card title="Twenty Template" icon="github" href="https://github.com/controlplane-com/templates/tree/main/twenty">
    View the source files, default values, and chart definition
  </Card>
</CardGroup>
