> ## 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.

# Documenso

> Deploy Documenso, the open-source e-signature platform with cryptographically signed PDFs and audit trails, on Control Plane. One location with bundled PostgreSQL, or highly available. Needs an existing GVC, a signing certificate, application keys and database credential secrets.

## Overview

Documenso is an open-source e-signature platform, a DocuSign alternative: upload a PDF, place signature fields, send it to recipients, and get back a PDF digitally signed with your own certificate plus an audit trail. This template deploys the Documenso server (UI, REST API, signing pages, and background jobs on port `3000`) with a bundled PostgreSQL, single-instance by default and highly available behind one flag. Documents are stored in the database or in an S3 bucket you own. Documenso runs in exactly one location of your GVC.

> [!NOTE]
>
> This template deploys into an existing [GVC](https://docs.controlplane.com/reference/gvc.md) that you already have — it does not create or manage a GVC. The `location` you set must already be one of that GVC's locations; if it is not, Documenso starts nothing in the GVC.

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-documenso` | Documenso server: UI, REST API, signing pages, and background jobs on port `3000` (standard, `documenso.replicas` replicas, pinned to `location`) | — |
| Workload | `RELEASE_NAME-postgres` | Bundled PostgreSQL from the [postgres](https://docs.controlplane.com/template-catalog/templates/postgres.md) template — all data, and the PDFs when `storage.type` is `database` (stateful, 1 replica) | `postgres.enabled: true` (default) |
| Workload | `RELEASE_NAME-postgres-backup` | Cron job that dumps the database to object storage on a schedule | `postgres.backup.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha` | Patroni PostgreSQL cluster from the [postgres-highly-available](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md) template (stateful, 3 replicas) | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-proxy` | HAProxy leader endpoint Documenso connects to | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-etcd` | etcd cluster Patroni uses for leader election | `postgresHA.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha-backup` | Backup job for the highly available database | `postgresHA.backup.enabled: true` |
| Volume set | `RELEASE_NAME-pg-vs` | PostgreSQL data directory | `postgres.enabled: true` (default) |
| Volume set | `RELEASE_NAME-postgres-ha-vs` | Patroni data volumes | `postgresHA.enabled: true` |
| Volume set | `RELEASE_NAME-etcd-vs` | etcd data volumes | `postgresHA.enabled: true` |
| Identity | `RELEASE_NAME-documenso-identity` | Bound to the Documenso workload; carries your AWS cloud account link in keyless S3 mode | — |
| Policy | `RELEASE_NAME-documenso-policy` | Grants that identity `reveal` on exactly the secrets Documenso reads | — |
| Identity | `RELEASE_NAME-postgres-ha-identity` | Bound to the Patroni PostgreSQL workloads | `postgresHA.enabled: true` |
| Policy | `RELEASE_NAME-postgres-ha-policy` | Grants that identity `reveal` on its credentials secret only | `postgresHA.enabled: true` |
| Identity | `RELEASE_NAME-etcd-identity` | Bound to the etcd workload | `postgresHA.enabled: true` |
| Policy | `RELEASE_NAME-etcd-policy` | Grants the etcd identity `reveal` on the secrets it reads | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-etcd-startup`, `RELEASE_NAME-patroni-startup`, `RELEASE_NAME-postgres-proxy-startup`, `RELEASE_NAME-wal-g-backup-script` | Startup and backup scripts for the highly available database (contain no credentials) | `postgresHA.enabled: true` |
| Secret | `RELEASE_NAME-postgres-config` | Backup bucket and region for the highly available database | `postgresHA.enabled: true` and `postgresHA.backup.enabled: true` |
| Identity | `RELEASE_NAME-pg-identity` | Bound to the bundled PostgreSQL and its backup job | `postgres.enabled: true` (default) |
| Policy | `RELEASE_NAME-pg-policy` | Grants the PostgreSQL identity `reveal` on its credentials secret only | `postgres.enabled: true` (default) |

Your signing certificate, application keys, and database credentials are **not** created by the template — see [Prerequisites](#prerequisites).

## Prerequisites

Three secrets must exist **before** installing, and the GVC should have **one location** (see the last step). The values never pass through Helm values.

**1. Create the signing certificate secret**

An [opaque secret](https://docs.controlplane.com/guides/create-secret/opaque.md) holding the base64 text of a PKCS#12 (`.p12`) file. Documenso signs every completed PDF with it. Use the default `openssl pkcs12 -export`. Reading the passphrase into a variable keeps it out of your shell history:

```bash theme={null}
read -rs P12_PASS && export P12_PASS
openssl req -x509 -newkey rsa:2048 -nodes -keyout private.key -out certificate.crt \
  -days 730 -subj "/CN=Documenso Signing"
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt -passout env:P12_PASS
base64 < certificate.p12 | tr -d '\n' | \
  cpln secret create-opaque --name SIGNING_CERT_SECRET_NAME --encoding plain -f -
```

Set `signing.certificateSecretName` to this name. Use a certificate from your own CA instead of the self-signed one above if signers must see a trusted signature.

**2. Create the application keys secret**

A [dictionary secret](https://docs.controlplane.com/guides/create-secret/dictionary.md) with four keys. `signingPassphrase` is the certificate's password from the previous step:

```bash theme={null}
cpln secret create-dictionary --name APP_KEYS_SECRET_NAME \
  --entry nextAuthSecret="$(openssl rand -hex 32)" \
  --entry encryptionKey="$(openssl rand -hex 32)" \
  --entry encryptionSecondaryKey="$(openssl rand -hex 32)" \
  --entry signingPassphrase="$P12_PASS"
```

Set `secrets.name` to this name. Back the keys up outside Control Plane and never replace `encryptionKey`: it protects two-factor secrets and API tokens.

**3. Create the database credentials secret**

The bundled PostgreSQL initializes with these values and Documenso connects with them:

```bash theme={null}
cpln secret create-dictionary --name DB_CREDENTIALS_SECRET_NAME \
  --entry username=documenso \
  --entry password="$(openssl rand -hex 24)" \
  --entry database=documenso
```

Set `postgres.config.credentialsSecretName` to this name (`postgresHA.config.credentialsSecretName` in highly available mode). Secret names are org-wide, so give each release its own secrets. The three values are part of Documenso's connection URL, so use letters, digits, `-` and `_` only.

**4. Use a GVC with one location**

Documenso runs only in the location named by `location`, which must already be a location of your GVC. The bundled PostgreSQL starts in every location of the GVC, so a GVC with a single location keeps one Documenso against one database.

> [!WARNING]
>
> Create all three secrets **before** installing. Installing without one reports success, but the deployment never becomes ready and `cpln logs` stays empty; `status.versions[].message` from the command below names the missing secret:
>
> ```bash theme={null}
> cpln workload get-deployments RELEASE_NAME-documenso --gvc GVC_NAME -o yaml
> ```
>
> Create the secret and the deployment recovers on its own (`cpln workload force-redeployment RELEASE_NAME-documenso --gvc GVC_NAME` skips the wait); see [Troubleshooting](#troubleshooting).

Optional features need their own setup first: an S3 bucket for documents (see [Storage Setup](#storage-setup)), a backup bucket (see [Backing Up](#backing-up)), and an SMTP relay (see [Email](#email)).

## Installation

Once your three secrets exist, install from the marketplace registry, pointing the keys at them and `location` at a location of your GVC:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/documenso \
  --version 1.0.0 --gvc GVC_NAME \
  --set location=LOCATION_NAME \
  --set secrets.name=APP_KEYS_SECRET_NAME \
  --set signing.certificateSecretName=SIGNING_CERT_SECRET_NAME \
  --set postgres.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME
```

Other ways to install and manage the template:

> [card] [UI](https://docs.controlplane.com/template-catalog/install-manage/ui.md)
>
> Browse, install, and manage templates visually

> [card] [CLI](https://docs.controlplane.com/template-catalog/install-manage/cli.md)
>
> Manage templates from your terminal

> [card] [Terraform](https://docs.controlplane.com/template-catalog/install-manage/terraform.md)
>
> Declare templates in your Terraform configurations

> [card] [Pulumi](https://docs.controlplane.com/template-catalog/install-manage/pulumi.md)
>
> Declare templates in your Pulumi programs

## Configuration

### Location

```yaml theme={null}
# The ONE location of your GVC that Documenso runs in; it must already be a GVC location.
location: aws-us-east-1
```

Use a bare location name such as `aws-us-east-1`. A link form (`//location/aws-us-east-1`), a list, or a comma-separated value is refused at render time.

### Documenso Server

```yaml theme={null}
documenso:
  image: documenso/documenso:v2.21.1  # published tags carry the leading v
  replicas: 1                         # stateless; 2+ keeps the UI up through a rolling restart
  publicUrl: ""                       # empty = canonical endpoint (http://localhost:3000 when private); custom domain with scheme, e.g. https://sign.example.com
  resources:
    minCpu: 500m
    maxCpu: 2000m
    minMemory: 1Gi
    maxMemory: 2Gi
```

### Prerequisite Secrets

```yaml theme={null}
secrets:
  name: my-documenso-secrets
signing:
  certificateSecretName: my-documenso-signing-cert
```

Both must exist before install — see [Prerequisites](#prerequisites). The database credentials secret is set under [Database](#database).

### Sign-up

```yaml theme={null}
signup:
  disabled: false        # open so you can create your account; set true right after
  allowedDomains: ""     # comma-separated, e.g. "example.com,acme.org"; empty = any
```

### Document Storage

```yaml theme={null}
storage:
  type: database              # database (PDFs stored in PostgreSQL) | s3
  documentSizeLimitMb: 5      # max single upload
  s3:
    bucket: my-documenso-bucket
    region: us-east-1
    endpoint: ""              # S3-compatible servers only, e.g. http://my-minio.GVC.cpln.local:9000
    forcePathStyle: false     # true for most S3-compatible servers (MinIO)
    cloudAccountName: my-documenso-cloud-account   # AWS keyless auth (used when auth.secretName is empty)
    policyName: my-documenso-s3-policy             # your bucket-scoped IAM policy (README Storage setup)
    auth:
      secretName: ""          # S3-compatible servers only: dictionary with accessKeyId + secretAccessKey, e.g. my-documenso-s3-keys
```

With `database`, every PDF grows the PostgreSQL volume, so raise `postgres.volumeset.capacity` for heavy use. With `s3`, complete [Storage Setup](#storage-setup) first.

### Storage Setup

Only for `storage.type: s3`. Both paths set `storage.s3.bucket` and `storage.s3.region`.

**AWS S3 (keyless)** — create the bucket and a Control Plane [cloud account](https://docs.controlplane.com/guides/create-cloud-account.md) for the AWS account, and set `storage.s3.cloudAccountName`. Create this IAM policy (replace `YOUR_BUCKET_NAME`), set `storage.s3.policyName` to its name, and leave `storage.s3.auth.secretName` empty.

```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/*"]
  }]
}
```

**S3-compatible server (such as MinIO)** — create the bucket, set `storage.s3.endpoint` with its scheme and fully qualified host (`http://MINIO_WORKLOAD_NAME.GVC_NAME.cpln.local:9000`), set `storage.s3.forcePathStyle: true`, then create the keys secret and set `storage.s3.auth.secretName` to its name:

```bash theme={null}
cpln secret create-dictionary --name S3_KEYS_SECRET_NAME \
  --entry accessKeyId=ACCESS_KEY \
  --entry secretAccessKey=SECRET_KEY
```

### Email

```yaml theme={null}
smtp:
  enabled: false              # off = no verification or signature-request mail
  host: smtp.example.com
  port: 587
  secure: false               # false = STARTTLS/plain (587); true = implicit TLS (465)
  fromAddress: documenso@example.com
  fromName: Documenso
  auth:
    secretName: ""            # optional dictionary with username + password, e.g. my-documenso-smtp
```

With SMTP off no mail is sent: verification and signature-request emails fail, so copy each signing link out of the UI and send it yourself. `fromAddress` and `fromName` are required even when SMTP is off. For an authenticated relay, create the dictionary and set `smtp.auth.secretName`:

```bash theme={null}
cpln secret create-dictionary --name SMTP_SECRET_NAME \
  --entry username=SMTP_USERNAME \
  --entry password=SMTP_PASSWORD
```

### Telemetry

```yaml theme={null}
telemetry:
  enabled: false              # true = upstream's PostHog startup event and hourly heartbeat
```

### Access

```yaml theme={null}
publicAccess:
  enabled: true               # signers are external, so signing links need the public endpoint
internalAccess:
  type: same-gvc              # none | same-gvc | same-org | workload-list
  workloads: []               # workload-list only; this release's own workloads are always added
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME
```

A change to either access knob takes a couple of minutes to take effect after the upgrade reports success.

### Database

```yaml theme={null}
# set postgresHA.enabled: true (and postgres.enabled: false) for near-zero-downtime upgrades and failover
postgres:
  enabled: true
  image: postgres:18
  config:
    # REQUIRED — MUST EXIST BEFORE INSTALL, or the deployment WEDGES silently. A `dictionary` with the
    # URL-safe keys username, password, database (see README Prerequisites).
    credentialsSecretName: my-documenso-db-credentials
  resources:
    minCpu: 250m
    maxCpu: 1000m
    minMemory: 512Mi
    maxMemory: 1Gi
  volumeset:
    capacity: 10              # GiB (minimum 10); raise it for `database` storage
```

Highly available mode runs three Patroni replicas, three etcd members, and an HAProxy leader endpoint. Enable exactly one of `postgres` and `postgresHA`:

```yaml theme={null}
postgresHA:
  enabled: false
  config:
    # REQUIRED in HA mode — MUST EXIST BEFORE INSTALL. Same `dictionary` shape as postgres.config above.
    credentialsSecretName: my-documenso-db-credentials
  replicas: 3
  resources:
    minCpu: 500m
    maxCpu: 1000m
    minMemory: 1Gi
    maxMemory: 2Gi
  volumeset:
    capacity: 10              # GiB per replica (minimum 10)
```

The `postgres.backup` and `postgresHA.backup` blocks pass straight through to the underlying templates — see [Backing Up](#backing-up).

## Connecting

| What | Address | Notes |
| - | - | - |
| Public UI and API | `status.canonicalEndpoint` from `cpln workload get RELEASE_NAME-documenso --gvc GVC_NAME -o yaml` | Your Documenso account |
| Private UI | `http://localhost:3000` after `cpln port-forward RELEASE_NAME-documenso 3000:3000 --gvc GVC_NAME` | For `publicAccess.enabled: false` |
| Internal | `http://RELEASE_NAME-documenso.GVC_NAME.cpln.local:3000` | From other workloads in the GVC |
| Health | `GET /api/health` (database and certificate), `GET /api/certificate-status` | No authentication |
| PostgreSQL | `RELEASE_NAME-postgres.GVC_NAME.cpln.local:5432`, or `RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local:5432` in HA mode | Values from the database credentials secret (`cpln secret reveal DB_CREDENTIALS_SECRET_NAME -o yaml`) |

Verify the install, including the signing certificate, through a tunnel:

```bash theme={null}
cpln port-forward RELEASE_NAME-documenso 3000:3000 --gvc GVC_NAME
curl -s http://localhost:3000/api/certificate-status
```

The response must be `{"isAvailable":true}`. `{"isAvailable":false}` means the certificate cannot be opened with `signingPassphrase`.

### First Run

Documenso has no admin bootstrap and requires a verified email before the first sign-in; without SMTP no verification mail is sent. Register on the public endpoint, then verify yourself and become an admin (use your credentials secret's `username` and `database`):

```bash theme={null}
cpln workload exec RELEASE_NAME-postgres --gvc GVC_NAME --container postgresql -- \
  psql -U documenso -d documenso \
  -c "UPDATE \"User\" SET \"emailVerified\" = NOW(), roles = '{ADMIN}' WHERE email = 'you@example.com';"
```

In HA mode run the same `psql` in `RELEASE_NAME-postgres-ha`, container `patroni-postgres`, adding `-h RELEASE_NAME-postgres-ha-proxy.GVC_NAME.cpln.local`. Then sign in and close sign-up by setting `signup.disabled: true` or `signup.allowedDomains`.

## Operations

### Backing Up

Database backups are off by default. Enable `postgres.backup.enabled: true` (or `postgresHA.backup.enabled: true`) and choose a `provider` of `aws`, `gcp`, or `minio`; the bucket, cloud account, and IAM setup are described in [Backing Up](https://docs.controlplane.com/template-catalog/templates/postgres.md#backing-up) for the single-instance database and [Backing Up](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md#backing-up) for highly available mode, and the `backup` blocks of `values.yaml` use the same keys. With `storage.type: s3` a database backup holds only object keys, so protect the bucket too with versioning or replication.

### Restoring a Backup

Restore steps belong to the underlying database templates: [Restoring a Backup](https://docs.controlplane.com/template-catalog/templates/postgres.md#restoring-a-backup) and [Restoring a Backup](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md#restoring-a-backup).

### Rotating a Secret

Rotating a referenced secret does not restart Documenso. After changing a value, redeploy:

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

### Scaling and Availability

`documenso.replicas` is a real knob: the app tier is stateless, and with two or more replicas the UI stays up through a rolling restart. For database availability, set `postgresHA.enabled: true` and `postgres.enabled: false` at install; a single-instance PostgreSQL can briefly restart, and Documenso with it, on the first `helm upgrade`.

### Troubleshooting

> [accordion] Deployment never becomes ready and the logs are empty
>
> **Symptom:** `cpln helm install` reported success, `cpln logs` returns nothing, and `cpln workload get-deployments RELEASE_NAME-documenso --gvc GVC_NAME -o yaml` shows `The secret SECRET_NAME no longer exists. Workload updates are paused until the secret is added or the reference to the secret removed.` under `status.versions[].message`.
>
> **Cause:** A prerequisite secret does not exist: the application keys, the signing certificate, or the database credentials. The message names the missing one.
>
> **Fix:** Create it (see [Prerequisites](#prerequisites)). The deployment recovers on its own once the secret exists; `cpln workload force-redeployment RELEASE_NAME-documenso --gvc GVC_NAME` skips the wait.

> [accordion] Documenso starts nothing and reports it is deactivated
>
> **Symptom:** The deployment message reads `This workload location is deactivated because maxScale is set to 0.`
>
> **Cause:** `location` is not one of the GVC's locations. The default is `aws-us-east-1`.
>
> **Fix:** Set `location` to a location your GVC already has, and run `cpln helm upgrade`.

> [accordion] First sign-in says Unverified email
>
> **Symptom:** After registering you land on the unverified-account page and cannot sign in.
>
> **Cause:** Email verification is mandatory and, without SMTP, no verification mail is sent.
>
> **Fix:** Run the SQL in [First Run](#first-run), or configure [Email](#email).

> [accordion] Certificate status returns isAvailable false
>
> **Symptom:** `/api/certificate-status` returns `{"isAvailable":false}` and `/api/health` reports `status: warning`.
>
> **Cause:** The `.p12` cannot be opened with the `signingPassphrase` in the application keys secret, or the secret does not hold the base64 text of a valid `.p12`.
>
> **Fix:** Recreate the certificate secret from [Prerequisites](#prerequisites) and make sure `signingPassphrase` matches its password, then run `cpln workload force-redeployment RELEASE_NAME-documenso --gvc GVC_NAME`. The boot line `Certificate not found or not readable` refers to a certificate file and can be ignored.

> [accordion] Render refuses the install
>
> **Symptom:** `cpln helm install` fails with a message beginning `documenso:`, such as `` `location` must be a bare location NAME `` or `enable exactly one database`.
>
> **Cause:** A value is missing or invalid: a link-form or list `location`, an unknown `storage.type`, `storage.s3` without credentials, or both or neither of `postgres.enabled` and `postgresHA.enabled`.
>
> **Fix:** Correct the value the message names and reinstall.

> [accordion] Large upload is refused
>
> **Symptom:** A PDF larger than a few megabytes is rejected on upload.
>
> **Cause:** `storage.documentSizeLimitMb` defaults to `5`.
>
> **Fix:** Raise `storage.documentSizeLimitMb` and run `cpln helm upgrade`.

## Important Notes

* **Close sign-up as soon as your account exists**: set `signup.disabled: true` or `signup.allowedDomains`.
* **Check the certificate after installing**: `/api/certificate-status` must return `{"isAvailable":true}`.
* **Never replace `encryptionKey`**: two-factor secrets and API tokens become unreadable.
* **Install into a GVC with one location** and set `location` to it; the bundled PostgreSQL starts in every GVC location.
* **Switching from keyless S3 to static keys on an existing release** leaves the AWS binding on the identity; reinstall to clear it.
* **Without SMTP no mail is sent**: send each signing link yourself.

## External References

> [card] [Documenso Self-Hosting Guide](https://docs.documenso.com/docs/self-hosting)
>
> Upstream guide to running and configuring Documenso

> [card] [Environment Variables](https://docs.documenso.com/docs/self-hosting/configuration/environment)
>
> Every setting the server reads

> [card] [Signing Certificate](https://docs.documenso.com/docs/developers/local-development/signing-certificate)
>
> How the signing certificate is built and used

> [card] [Documenso on GitHub](https://github.com/documenso/documenso)
>
> Source code and issue tracker
