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

# Logto

> Logto open-source auth platform (OIDC, OAuth, users, roles, organizations, Admin Console) on Control Plane. Two replicas on a bundled highly available PostgreSQL by default, or a single PostgreSQL for dev. Deploys into an existing GVC; admin and database dictionary secrets must exist first.

## Overview

Logto is an open-source sign-in and user-management platform. Your apps send users to Logto for sign-in and sign-up over OIDC or OAuth, and Logto provides users, roles, organizations, social and passwordless connectors, MFA, and a Management API, with an Admin Console to manage them. This template runs two interchangeable Logto replicas on a bundled highly available PostgreSQL by default, serves the Admin Console from its own workload, and creates the Admin Console account from a secret you provide.

All state, including signing keys and the admin account, lives in PostgreSQL. Replicas share only the database.

> [!NOTE]
>
> **This template deploys into an existing GVC.** It does not create or manage one. Every resource lands in the GVC you install into. Use a single-location GVC: the bundled database runs in one location.

### What Gets Created

| Resource | Name | Purpose | Optional |
| - | - | - | - |
| Workload | `RELEASE_NAME-logto` | Sign-in pages, OIDC and the Management API on port 3001 (standard, 2 replicas) | — |
| Workload | `RELEASE_NAME-logto-admin` | Admin Console on port 3002 and its own public URL (standard, 1 replica) | `adminConsole.publicAccess.enabled: true` |
| Workload | `RELEASE_NAME-postgres-ha` | PostgreSQL cluster managed by Patroni (stateful, 3 replicas) | default store (`postgresHA.enabled: true`) |
| Workload | `RELEASE_NAME-postgres-ha-proxy` | HAProxy leader endpoint that Logto connects through | default store |
| Workload | `RELEASE_NAME-etcd` | Consensus store for Patroni (stateful, 3 replicas) | default store |
| Workload | `RELEASE_NAME-postgres` | Single-instance PostgreSQL (stateful, 1 replica) | `postgres.enabled: true` |
| Volume set | `RELEASE_NAME-postgres-ha-vs`, `RELEASE_NAME-etcd-vs` | Persistent data for the Patroni and etcd replicas | default store |
| Volume set | `RELEASE_NAME-pg-vs` | Persistent data for the single-instance store | `postgres.enabled: true` |
| Domain | your `customDomain.core` and `customDomain.adminConsole` hostnames | Custom hostnames routed to the matching workload | `customDomain.core` or `customDomain.adminConsole` set |
| Secret | your `admin.secretName` | Admin account `username` and `password`. **Created by you** — see [Prerequisites](#prerequisites) | — |
| Workload | `RELEASE_NAME-postgres-ha-backup` | Scheduled logical backups of the HA store | `postgresHA.backup.enabled: true` with `mode: logical` |
| Workload | `RELEASE_NAME-postgres-backup` | Scheduled backups of the single-instance store | `postgres.backup.enabled: true` |
| Secret | `RELEASE_NAME-postgres-config` | Non-sensitive backup destination settings for the HA store | `postgresHA.backup.enabled: true` |
| Secret | your `postgresHA.config.credentialsSecretName` or `postgres.config.credentialsSecretName` | Database `username`, `password` and `database`. **Created by you** — see [Prerequisites](#prerequisites) | — |
| Secret | `RELEASE_NAME-patroni-startup`, `RELEASE_NAME-postgres-proxy-startup`, `RELEASE_NAME-etcd-startup`, `RELEASE_NAME-wal-g-backup-script` | Startup and backup scripts for the HA store | default store |
| Identity + Policy | `RELEASE_NAME-logto-identity`, `RELEASE_NAME-logto-policy` | Grants Logto `reveal` on exactly two secrets: your admin secret and your database credentials secret | — |
| Identity + Policy | `RELEASE_NAME-pg-identity`, `RELEASE_NAME-pg-policy` | Least-privilege access for the single-instance store | `postgres.enabled: true` |
| Identity + Policy | `RELEASE_NAME-postgres-ha-identity`, `RELEASE_NAME-postgres-ha-policy`, `RELEASE_NAME-etcd-identity`, `RELEASE_NAME-etcd-policy` | Least-privilege access for the HA store's own workloads | default store |

## Prerequisites

**Two dictionary secrets must exist before you install.** The template creates neither; it references yours by name and grants Logto `reveal` on exactly those secrets.

**1. Create the admin account secret**

A [dictionary secret](https://docs.controlplane.com/guides/create-secret/dictionary.md) with exactly the keys `username` and `password`. Logto creates the Admin Console account from it on first boot:

```bash theme={null}
cpln secret create-dictionary --name ADMIN_SECRET_NAME \
  --entry username=admin \
  --entry password="$(openssl rand -hex 32)"
```

The username may contain letters, digits and `_`, and must start with a letter or `_`. The password needs 8 or more characters and must not be a known breached password. Set `admin.secretName` to this name.

**2. Create the database credentials secret**

A dictionary secret holding `username`, `password` and `database`. The bundled PostgreSQL initializes with them and Logto connects with them:

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

All three values must be URL-safe (letters, digits, `-`, `_`, `.`). Set `postgresHA.config.credentialsSecretName` (HA, the default) or `postgres.config.credentialsSecretName` (single-instance) to this name. Secret names are org-wide, so give each release its own secrets.

**3. Read the admin password back when you need it**

`-o yaml` is required; without it the command prints only a summary table:

```bash theme={null}
cpln secret reveal ADMIN_SECRET_NAME -o yaml
```

> [!WARNING]
>
> **A missing prerequisite secret wedges the install silently.** The install exits 0, every resource is created, and Logto then waits. Because the container never ran, `cpln logs` returns nothing.
>
> The only place the missing secret is named is `status.versions[].message`:
>
> ```bash theme={null}
> cpln workload get-deployments RELEASE_NAME-logto --gvc GVC_NAME -o yaml
> ```
>
> ```text theme={null}
> The secret ADMIN_SECRET_NAME no longer exists. Workload updates are paused until
> the secret is added or the reference to the secret removed.
> ```
>
> It is **`get-deployments`** — plain `cpln workload get` has no `versions` key. Once you create the secret the deployment recovers on its own; to skip the wait, force a redeployment:
>
> ```bash theme={null}
> cpln workload force-redeployment RELEASE_NAME-logto --gvc GVC_NAME
> ```

**Custom domains (optional).** The apex domain must be [registered with Control Plane](https://docs.controlplane.com/guides/configure-domain.md), and each hostname needs a CNAME to the GVC endpoint before you install. See [Custom Domains](#custom-domains).

**Backups (optional).** A cloud account and bucket, set up as described on the [postgres-highly-available](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md) page. See [Backing Up](#backing-up).

## Installation

Install from the marketplace registry, naming your two secrets:

```bash theme={null}
cpln helm install RELEASE_NAME oci://ghcr.io/controlplane-com/templates/logto \
  --version 1.0.0 --gvc GVC_NAME \
  --set admin.secretName=ADMIN_SECRET_NAME \
  --set postgresHA.config.credentialsSecretName=DB_CREDENTIALS_SECRET_NAME
```

For the single-instance store set `postgresHA.enabled=false`, `postgres.enabled=true` and `postgres.config.credentialsSecretName` instead. Choose `publicAccess` and the custom domains now: the public URL is the OIDC issuer.

To install using another method, follow the instructions for your preferred tool:

> [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

### Logto

```yaml theme={null}
image: ghcr.io/logto-io/logto:1.45.0

# Logto replicas (sign-in pages, OIDC and the Management API). All state lives in
# PostgreSQL, so replicas are interchangeable. 2 keeps sign-in up through restarts and
# upgrades; 1 is the lighter shape. Allowed: 1-4.
replicas: 2

resources: # per Logto process (each replica, and the Admin Console workload)
  minCpu: 250m
  minMemory: 512Mi
  maxCpu: 1000m
  maxMemory: 1Gi
```

### Admin account

```yaml theme={null}
# ─── Admin Account ────────────────────────────────────────────────────────────
# REQUIRED PREREQUISITE SECRET — CREATE IT BEFORE YOU INSTALL. A `dictionary`
# secret with exactly two keys, `username` (letters, digits, _; starts with a
# letter or _) and `password` (8+ characters; use `openssl rand -hex 32`). Logto
# creates the Admin Console account from it on FIRST boot only; later changes to
# the secret change nothing. If it does not exist the deployment WEDGES silently.
admin:
  secretName: my-logto-admin
```

### Access

```yaml theme={null}
publicAccess:
  enabled: true # sign-in pages + OIDC on the canonical *.cpln.app URL; false = reach Logto only via cpln port-forward

adminConsole:
  publicAccess:
    # true = a second workload serves the Admin Console on its own *.cpln.app URL (sign-in only).
    # false = no public Console; use `cpln port-forward RELEASE_NAME-logto 3002:3002` instead.
    enabled: true
```

### Custom Domains

Bare hostnames; the chart creates the domains. The core hostname becomes the OIDC issuer, so set it at install.

```yaml theme={null}
customDomain: # bare hostnames; the chart creates the domains (apex must be verified, CNAME required — see Custom Domains)
  core: ""         # e.g. auth.example.com; empty = canonical *.cpln.app. Becomes the OIDC issuer — set it at install
  adminConsole: "" # e.g. auth-admin.example.com; needs adminConsole.publicAccess.enabled
```

### Internal Access and Webhooks

```yaml theme={null}
internalAccess: # which workloads inside Control Plane can call Logto
  type: same-gvc # options: none, same-gvc, same-org, workload-list
  workloads: [] # used with workload-list
  # workloads:
  #   - //gvc/GVC_NAME/workload/WORKLOAD_NAME

webhooks:
  # true = webhooks may call workloads inside Control Plane (*.cpln.local URLs). Off, Logto
  # refuses internal addresses and sends webhooks only to public URLs.
  allowInternalTargets: false
```

`webhooks.allowInternalTargets` is a security trade-off. By default Logto sends webhooks only to public URLs. Turning it on lets anyone who can manage webhooks (a Console admin, or an app with Management API access) make Logto call any workload it can reach inside Control Plane. Leave it off unless you need internal receivers.

### Database

Highly available PostgreSQL is the default. For a lighter install, set `postgresHA.enabled: false` and `postgres.enabled: true`. The `backup` blocks pass through to the [postgres-highly-available](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md) and [postgres](https://docs.controlplane.com/template-catalog/templates/postgres.md) templates.

```yaml theme={null}
# ─── Database: Highly Available PostgreSQL (default) ──────────────────────────
# Deploys the postgres-highly-available template: 3 Patroni replicas, 3 etcd
# replicas, and an HAProxy leader endpoint Logto connects to.
postgresHA:
  enabled: true
  config:
    # REQUIRED PREREQUISITE SECRET (HA mode) — CREATE IT BEFORE YOU INSTALL. If it
    # does not exist the deployment WEDGES silently and looks like a platform fault.
    # A `dictionary` secret with exactly three URL-safe keys: username, password,
    # database. PostgreSQL reads it once, when its data is first initialized, and
    # Logto connects with the same keys. See Prerequisites for the command.
    credentialsSecretName: my-logto-db-credentials
  replicas: 3
  image: controlplanecorporation/patroni-postgres:0.8 # pgvector 0.8.7 (CVE-2026-103484); tags 0.1-0.7 are refused
  volumeset:
    capacity: 10 # initial capacity in GiB per replica (minimum is 10)

  backup: # pass-through to the postgres-highly-available template's backup feature
    enabled: false
    mode: logical # logical or wal-g
    resources:
      cpu: 100m
      memory: 512Mi # 128Mi OOM-kills wal-g mid-backup
    logical:
      image: ghcr.io/controlplane-com/backup-images/postgres-backup:17.1.0
      schedule: "0 2 * * *" # daily at 2am UTC
    walg:
      intervalSeconds: 21600 # every 6 hours
    provider: aws # Options: aws, gcp, or minio
    aws:
      bucket: my-logto-backup-bucket
      region: us-east-1
      cloudAccountName: my-logto-backup-cloudaccount
      policyName: my-logto-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-logto-backup-bucket
      cloudAccountName: my-logto-backup-cloudaccount
      prefix: postgres/backups
    minio: # any S3-compatible endpoint, e.g. an internal MinIO template deployment
      endpoint: http://my-minio-workload:9000
      bucket: my-logto-backup-bucket
      # REQUIRED PREREQUISITE SECRET when provider is `minio` — a `dictionary`
      # secret holding exactly `accessKey` and `secretKey` (see the postgres-highly-available docs).
      credentialsSecretName: my-logto-minio-credentials
      prefix: postgres/backups
```

```yaml theme={null}
# ─── Database: Single-instance PostgreSQL (dev/lightweight) ───────────────────
# Enable this and disable postgresHA for a lighter non-HA deployment.
postgres:
  enabled: false
  config:
    # REQUIRED PREREQUISITE SECRET (single-instance mode) — the same kind of
    # `dictionary` secret as postgresHA.config.credentialsSecretName above
    # (keys: username, password, database). Create it BEFORE you install.
    credentialsSecretName: my-logto-db-credentials
  volumeset:
    capacity: 10 # initial capacity in GiB (minimum is 10)

  backup: # pass-through to the postgres template's backup feature (Postgres 17+)
    enabled: false
    image: ghcr.io/controlplane-com/backup-images/postgres-backup:18.1.0
    schedule: "0 2 * * *" # daily at 2am UTC
    resources:
      cpu: 100m
      memory: 128Mi
    provider: aws # Options: aws, gcp, or minio
    aws:
      bucket: my-logto-backup-bucket
      region: us-east-1
      cloudAccountName: my-logto-backup-cloudaccount
      policyName: my-logto-backup-policy
      prefix: postgres/backups
    gcp:
      bucket: my-logto-backup-bucket
      cloudAccountName: my-logto-backup-cloudaccount
      prefix: postgres/backups
    minio:
      endpoint: http://my-minio-workload:9000
      bucket: my-logto-backup-bucket
      # REQUIRED prerequisite dictionary secret holding `accessKey` and `secretKey`,
      # only when backup is enabled with provider: minio (see the postgres template docs).
      credentialsSecretName: my-logto-minio-credentials
      prefix: postgres/backups
```

## Connecting

Logto serves two URLs. Give each audience only its own:

| Who | URL | What they do there |
| - | - | - |
| Administrators | `https://CUSTOM_ADMIN_HOSTNAME/console` when `customDomain.adminConsole` is set; otherwise the canonical endpoint of `RELEASE_NAME-logto-admin` + `/console` | The Admin Console: applications, users, sign-in experience, connectors, MFA, roles. Sign in with the admin secret's `username` and `password`. |
| Your app's end users | `https://CUSTOM_CORE_HOSTNAME` when `customDomain.core` is set; otherwise the canonical endpoint of `RELEASE_NAME-logto` | Nothing directly: your app redirects them to Logto's sign-in and sign-up pages here and back. Opened on its own, the URL shows an error page, because no app started a sign-in. To try sign-in as a user, open `/demo-app` on this URL. |
| Your applications (OIDC) | Issuer: the end users' URL above (the core hostname when set) + `/oidc` (discovery at `/oidc/.well-known/openid-configuration`) | Paste the issuer, App ID and App secret from Admin Console, Applications into your app. |
| Workloads in the same GVC | `http://RELEASE_NAME-logto.GVC_NAME.cpln.local:3001` | The same endpoints over the internal network. |
| Admin Console without public access | `http://localhost:3002/console` through the port-forward below | Only with `adminConsole.publicAccess.enabled: false`. |

Find a canonical endpoint in `status.canonicalEndpoint`:

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

For a private Admin Console, open a tunnel and browse to `http://localhost:3002/console`:

```bash theme={null}
cpln port-forward RELEASE_NAME-logto 3001:3001 3002:3002 --gvc GVC_NAME
```

### First Run

Open the Administrators URL from the table above, sign in with the admin secret's `username` and `password`, then create an application under Applications and copy its App ID, App secret and the issuer into your app. The Admin Console serves nothing until the admin account has been created from your secret.

## Operations

### Backing Up

Enable the `backup` block of the database you use (`postgresHA.backup` or `postgres.backup`). Bucket and cloud-account setup is on the [postgres-highly-available](https://docs.controlplane.com/template-catalog/templates/postgres-highly-available.md) and [postgres](https://docs.controlplane.com/template-catalog/templates/postgres.md) pages. Restoring a Logto database from a backup is not documented for this template.

### Scaling and Availability

`replicas` (1 to 4) sets the number of Logto replicas; they are interchangeable because all state is in PostgreSQL. Two keep sign-in up through restarts and upgrades. A database failover interrupts sign-in for about a minute while PostgreSQL promotes a new leader; Logto reconnects on its own.

### Troubleshooting

> [accordion] Logto never starts and cpln logs returns nothing
>
> **Cause:** the admin secret or the database credentials secret does not exist.
>
> **Fix:** read `status.versions[].message` as shown in [Prerequisites](#prerequisites), create the secret, then run `cpln workload force-redeployment RELEASE_NAME-logto --gvc GVC_NAME`.

> [accordion] The Admin Console workload is ready but its URL returns 503
>
> **Cause:** the Admin Console workload waits for the admin account (`waiting for the first admin account` in its log). The account is created by the `RELEASE_NAME-logto` workload.
>
> **Fix:** look for `FATAL bootstrap` in the `RELEASE_NAME-logto` logs:
>
> ```bash theme={null}
> cpln logs '{gvc="GVC_NAME", workload="RELEASE_NAME-logto"} |= "FATAL"' --limit 50 --since 10m
> ```

> [accordion] FATAL bootstrap with password.rejected
>
> **Cause:** the admin password is a known breached password, is too weak, or `api.pwnedpasswords.com` is unreachable; Logto refuses the password when the check cannot run.
>
> **Fix:** correct the password in the admin secret (and allow outbound HTTPS to `api.pwnedpasswords.com`), then run `cpln workload force-redeployment RELEASE_NAME-logto --gvc GVC_NAME`.

> [accordion] A port-forwarded Admin Console signs in but shows no data
>
> **Cause:** with the public Admin Console on, Logto accepts only the public Console origin.
>
> **Fix:** use the public Console URL, or set `adminConsole.publicAccess.enabled: false` and use the port-forward.

> [accordion] The Admin Console URL shows the sign-in service instead of the Console
>
> **Cause:** with custom domains set, the `*.cpln.app` Admin Console URL serves the sign-in service.
>
> **Fix:** use the `customDomain.adminConsole` hostname for administrators and the `customDomain.core` hostname for users.

## Important Notes

* **The admin secret is read once**, when the database is first initialized. Afterwards change the admin password in the Admin Console; editing the secret changes nothing. There is no supported reset for a lost admin password, so store it safely.
* **Choose `publicAccess` and the custom domains at install.** The public URL is the OIDC issuer, and changing it later breaks every app configured against it.
* **First boot needs outbound HTTPS to `api.pwnedpasswords.com`.** Logto checks the admin password against known breaches and refuses it if the check cannot run.
* **To send verification and sign-in emails, add an email connector** (SMTP or a provider) under Admin Console, Connectors. Logto reaches the mail server over outbound TCP, which the template allows.
* **Leave `webhooks.allowInternalTargets` off unless you need internal receivers.** Turning it on lets anyone who can manage webhooks make Logto call any workload it can reach inside Control Plane.
* **With custom domains set, give administrators and users only the domain URLs.** The `*.cpln.app` Admin Console URL then serves the sign-in service instead of the Console.
* **Uninstalling removes the domain resources.** Reinstalling recreates them, and no re-verification is needed while the apex domain stays registered.
* **The first `helm upgrade` after an install can restart the bundled database**, which briefly interrupts sign-in.
* **Uninstalling deletes the database volumes**, and with them every user and signing key. Enable backups.
* **The open-source edition has one Admin Console account.** Inviting more admins is a paid Logto feature.

## External References

> [card] [Logto Documentation](https://docs.logto.io/)
>
> Official Logto documentation

> [card] [Logto OSS Configuration](https://docs.logto.io/concepts/core-service/configuration)
>
> Core service configuration reference

> [card] [Integrate an App](https://docs.logto.io/quick-starts)
>
> Quick starts for connecting your app to Logto

> [card] [Logto on GitHub](https://github.com/logto-io/logto)
>
> Upstream source and releases

> [card] [Logto Template](https://github.com/controlplane-com/templates/tree/main/logto)
>
> View the source files, default values, and chart definition
