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

# Databases

> Managed PostgreSQL and Valkey clusters — provision one, connect to its endpoint, grow it into a high-availability topology, and restore it from a backup.

The database service runs managed clusters for you. You pick an engine, a size
and a subnet; the platform provisions the members, publishes a stable hostname
for connections, takes scheduled backups, and moves the endpoint when the
primary changes.

The service is **regional** — `database.sa-saopaulo-1.basaltic.sh`. A cluster
lives in one region, on one of your subnets.

<CardGroup cols={2}>
  <Card title="Create a cluster" icon="database" href="#creating-a-cluster">
    Engine, flavor, storage and placement — and what a create refuses before
    it provisions anything.
  </Card>

  <Card title="Connect" icon="plug" href="#connecting">
    The endpoint you are given, where its password lives, and why you connect
    by name.
  </Card>

  <Card title="High availability" icon="git-branch" href="#high-availability">
    Converting a single node in place, adding replicas, and what a failover
    actually does.
  </Card>

  <Card title="Backups and restore" icon="rotate-ccw" href="#backups-and-restore">
    The schedule, the retention window, and restoring in place versus into a
    new cluster.
  </Card>

  <Card title="Parameter groups" icon="sliders" href="#parameter-groups">
    What you may tune, and when a change actually reaches the engine.
  </Card>

  <Card title="Access control" icon="shield" href="#access-control">
    Actions, CRNs, and the tag condition that will not do what you expect.
  </Card>
</CardGroup>

## Engines and versions

`GET /v1/engines` is the catalogue. You choose a **major version only** — minor
and patch releases inside that major are applied for you.

| Engine     | Versions   | Default | Port |
| ---------- | ---------- | ------- | ---- |
| `postgres` | `17`, `18` | `17`    | 5432 |
| `valkey`   | `8`        | `8`     | 6379 |

Omit `engine_version` on create and you get the engine's `default_version` from
that catalogue. Read it from the API rather than assuming — the default moves
as majors are added.

<Warning>
  The engine and its major version are **fixed for the life of the cluster**.
  There is no in-place major upgrade. Moving from `17` to `18` means creating a
  new cluster — see [restoring into a new cluster](#restoring-into-a-new-cluster)
  for the path that carries your data across.
</Warning>

### What each engine supports

The two engines are not the same product with a different port. Most of the
day-2 surface is postgres-only, because it is what the topology underneath can
actually do.

|                                           | `postgres`               | `valkey`                             |
| ----------------------------------------- | ------------------------ | ------------------------------------ |
| Node counts                               | `1`, or `2`–`10` for HA  | `1`, or `3` for HA (`2` is rejected) |
| Convert a single node to HA               | Yes                      | No                                   |
| Add or remove a replica on a live cluster | Yes                      | No — the topology is fixed at create |
| Logical databases and database users      | Yes                      | No                                   |
| Restore (in place and at create)          | Yes                      | No                                   |
| Backup kinds                              | `base` and `incremental` | `base` only                          |

<Note>
  A valkey cluster is either a single node or a three-member quorum. Two
  members cannot hold a stable quorum, so `instance_count: 2` is refused rather
  than provisioned into something that cannot fail over.
</Note>

## Creating a cluster

<Tabs>
  <Tab title="Console">
    Go to **Databases → Clusters** and choose **Create cluster**.

    **Details** takes the **Name** and an optional **Description**. **Engine**
    picks the engine, its **Version**, and optionally a **Parameter group**.
    Choose a **Flavor**, then **Capacity** for **Storage (GB)** and **Nodes**.
    **Access** sets the **Admin user** and, on postgres, the **Default
    database**. **Networking** takes the **Subnet**, an optional **Fixed IP
    address** and the **Security groups**.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST https://database.sa-saopaulo-1.basaltic.sh/v1/clusters
    {
      "name": "prod-orders-db",
      "engine_type": "postgres",
      "engine_version": "17",
      "flavor_id": "e5f6a7b8-c9d0-4123-e4f5-a6b7c8d9e0f1",
      "storage_gb": 100,
      "instance_count": 1,
      "networks": [{ "subnet_id": "d4e5f6a7-b8c9-4012-d3e4-f5a6b7c8d9e0" }],
      "assign_public_ip": true
    }
    ```
  </Tab>
</Tabs>

`name`, `engine_type`, `flavor_id`, `storage_gb` and `networks` are required.
The response is **`202`** with the cluster in `pending`; poll
`GET /v1/clusters/{cluster_id}` until `status` is `active`.

<Steps>
  <Step title="Pick a database-family flavor">
    Cluster members book flavors reserved for this product:

    ```bash theme={null}
    GET https://compute.sa-saopaulo-1.basaltic.sh/v1/flavors?family=database
    ```

    A `general` or `loadbalancer` flavor is refused with a `400` naming the
    family it found. The check runs before anything is reserved, so a wrong
    flavor costs you a round trip and nothing else.
  </Step>

  <Step title="Size the storage">
    `storage_gb` is the disk given to **each member**, so a three-node cluster
    of `storage_gb: 100` consumes three 100 GB volumes.

    <Warning>
      Storage, flavor, engine and node count are **immutable** through
      `PATCH /v1/clusters/{cluster_id}` — that path edits `description`, `tags`
      and the parameter-group binding only. Size for where you are going.
    </Warning>
  </Step>

  <Step title="Place it on a subnet">
    `networks` takes at least one interface, each naming a `subnet_id` and
    optionally `security_group_ids`. Every member lands on the same subnets,
    and a replica added later inherits that placement. See
    [Networking](/networking) for subnets and security groups.
  </Step>

  <Step title="Choose the exposure">
    `assign_public_ip` defaults to **`true`**: each endpoint gets a floating IP
    and is reachable from the internet. `false` allocates no floating IP —
    endpoints resolve to member addresses inside the VPC, which is how you
    place a cluster on a private subnet. The console control is the **Public
    endpoints** checkbox under **Endpoint exposure**, ticked by default for
    the same reason.

    <Warning>
      A public cluster requires its subnet to carry a default route
      (`0.0.0.0/0`) to an internet gateway. Without one the create is refused
      up front — attach a gateway and add the route, or set
      `assign_public_ip: false`. The console checks the chosen subnet's route
      table first and warns you before you submit.
    </Warning>

    The setting governs IPv4 only. IPv6 reachability follows the subnet's
    `::/0` route either way, exactly as it does for any other resource.
  </Step>

  <Step title="Wait for active">
    Members report their own readiness once the engine is up, and the cluster
    flips `building` to `active` when the whole set has reported. First boot
    installs and initialises the engine, so allow minutes, not seconds.
  </Step>
</Steps>

### Naming, quota, and where the members show up

<ResponseField name="name" type="unique per account">
  Matches `^[a-zA-Z0-9](?:[a-zA-Z0-9_.\-]{0,126}[a-zA-Z0-9])?$`. It becomes the
  first label of the cluster's endpoint hostname, so a DNS-friendly name gives
  you a DNS-friendly endpoint.
</ResponseField>

<ResponseField name="quota" type="database / instances, per region">
  Quota counts **members**, not clusters — a three-node cluster consumes three.
  Adding a replica reserves one more before the VM is created.
</ResponseField>

<ResponseField name="members" type="visible in compute, read-only">
  Each member is an instance in your account and appears in
  `GET /v1/instances` with `managed_by: "database"`. It is there to be seen,
  not driven: start, stop, reboot and delete against a managed instance all
  `404`. Manage the cluster through this API.
</ResponseField>

<Note>
  `key_names` is refused on a database cluster. The members run the platform's
  own software and there is no SSH path to them — the endpoint is the whole
  tenant-facing surface.
</Note>

## Connecting

A cluster publishes its connection points in `endpoints`:

| Kind     | Exists when                          | Points at           |
| -------- | ------------------------------------ | ------------------- |
| `writer` | Always                               | The current primary |
| `reader` | The cluster has at least one replica | One live replica    |

Each entry carries `dns_name`, `port`, and `ip_address` — the address the name
currently answers with.

<Warning>
  **Connect by `dns_name`, never by `ip_address`.** The address is resolved per
  request and is not stable: on a VPC-only cluster the writer's address changes
  the moment the primary changes. Use `ip_address` for the things a name cannot
  express, such as a security-group rule.
</Warning>

The name itself is deterministic — the cluster's name and your account handle
inside a per-region database zone, with the reader carrying a `-ro` suffix on
the cluster label:

```
<cluster-name>.<account-handle>.<regional database zone>
<cluster-name>-ro.<account-handle>.<regional database zone>
```

The zone suffix belongs to the region, so take the finished value from
`endpoints[].dns_name` rather than assembling it yourself.

### Credentials

The cluster is created with a bootstrap admin role named by `admin_user`
(default `admin`) and, on postgres, a database named by `default_database`
(default `default`).

Its password is not returned by the API. It is stored in the secrets service,
and the cluster reports the id: read `admin_secret_id` from the cluster, then
fetch that secret. The console shows the same id as **Admin password secret**
on the cluster, linked to the secret itself. The same holds for every database user you create —
`password_secret_id` points at the secret that holds it.

<Warning>
  A database user's plaintext password is returned **once**, on the create and
  `rotate-password` responses only. `GET` and `LIST` never include it. Capture
  it, or fetch it from the secret it points at.
</Warning>

Password authentication is SCRAM-SHA-256 for postgres.

<Warning>
  A cluster created with `assign_public_ip: true` answers on the public
  internet. Nothing else fences it — put a security group on the members'
  interfaces that admits only the sources you intend, and confirm the rule is
  attached to the interface rather than merely defined. See
  [Networking](/networking).
</Warning>

## High availability

A cluster is HA when it runs more than one member. Postgres starts HA at two
members and grows to ten; valkey is HA only at three.

You do not have to decide on day one. A single postgres node is the cheap first
choice, and there is a path from it to HA that keeps the endpoint and the data.

### Converting a single node

<Tabs>
  <Tab title="Console">
    Open the cluster and choose **Convert to HA**, then **Convert**. The
    button only appears on a postgres cluster that is not HA-capable yet — on
    one that already is, the same slot shows **Add replica** instead.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters/{cluster_id}/convert-to-ha
    ```
  </Tab>
</Tabs>

This adopts a running single-node **postgres** cluster into an HA-capable one
in place. It keeps the cluster's endpoint, its data and everything attached to
it — the alternative was dumping into a second cluster and re-pointing every
connection string by hand.

<Warning>
  **This restarts the engine, on your only copy of the data.** A single-node
  cluster has no replica to serve while its node comes back up under the new
  supervisor. Convert during a window where a brief outage is acceptable.
</Warning>

The call answers `202` and the cluster reports `converting`. It returns to
`active` when the member reports the outcome, and is marked HA-capable
(`patroni_managed: true`) **only on success** — a conversion that fails leaves
the node serving exactly as it was. A conversion that is never reported is
abandoned after 30 minutes rather than holding the cluster in `converting`
forever.

It is refused with `409` if the cluster is already HA-capable, a conversion is
already in flight, the cluster is not `active`, or it does not have exactly one
member.

### Replicas

<Tabs>
  <Tab title="Console">
    **Add replica** on the cluster provisions one. To remove one, open the
    **Nodes** tab and use the row action on the member you want gone —
    confirmed as **Remove replica**. The action is absent on the row whose
    **Role** is primary — the rule below, made visible.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST   /v1/clusters/{cluster_id}/replicas
    DELETE /v1/clusters/{cluster_id}/replicas/{instance_id}
    ```
  </Tab>
</Tabs>

A replica is a streaming read replica: another member, on the same flavor,
storage size and subnets as the primary, that seeds itself from the leader and
then follows it. The request body is empty today.

<Note>
  Adding a replica requires the cluster to be **HA-capable**, which is not the
  same question as its node count. A cluster created single-node has nothing
  for a replica to join and is refused with a pointer to `convert-to-ha`; a
  converted cluster has one member and accepts replicas immediately. The flag
  to read is `patroni_managed`, not `instance_count`.
</Note>

The first replica also brings up the **reader endpoint**, so a cluster that
grew into HA ends up with the same `-ro` hostname as one that was born HA.

Removing a replica requires the cluster to be `active`, and the primary can
never be removed — fail over first, which demotes it, then remove it. Valkey
refuses removals that would leave it at two members.

### Failover

<Tabs>
  <Tab title="Console">
    **Failover** on the cluster opens **Trigger failover**; confirming with
    **Failover** starts the switchover. The button only appears on an HA
    cluster.

    <Note>
      The console always fails over **untargeted** — it sends no
      `target_member`, so the engine picks the candidate. Promoting a
      *specific* member is **API only**. Read the member's name from the
      **Member** column on the **Nodes** tab and pass it below.
    </Note>
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters/{cluster_id}/failover
    { "target_member": "dbaas-9b2e4c7a-1f3d-4a8e-bc25-6d0f1a2b3c4d-1" }
    ```
  </Tab>
</Tabs>

This is a **planned switchover**, not a forced promotion, and it is safe to
trigger deliberately — that is what it is for. Use it to drain a member before
maintenance, or to prove your failover story before you need it.

Leave `target_member` out and the engine picks a candidate. Supply one — the
`member_name` of any member from the cluster's `instances` array — and that
member is promoted. Naming a member that is not in the cluster, or the current
primary, is a `400`.

What moves:

* The **writer endpoint follows the new primary.** On a public cluster the
  floating IP is rebound, so the address does not even change. On a VPC-only
  cluster the record is republished at the new primary's address — which is
  why you connect by name.
* The **reader endpoint is re-pinned** off the newly promoted member onto
  another live replica.

The cluster reports `failing-over` while the switchover is pending, and returns
to `active` when the transition is reported. A pending switchover that is not
executed within **10 minutes** is cleared and the cluster rolls back to
`active` — retry it.

The cluster must be `active` and HA. A single-node cluster has nothing to
switch to and is refused.

## Users and databases

<Note>
  This whole section is postgres-only. Valkey has no logical databases and no
  per-user management — `engine "valkey" has no logical databases or db users`
  is the refusal. Connect to a valkey cluster with its `admin_user` and the
  admin secret.
</Note>

<Tabs>
  <Tab title="Console">
    **Create user** on the cluster takes a **Name** and an optional
    **Permissions** document. **Create database** takes a **Name**, and
    optionally an **Encoding** and a **Collation**. Both buttons sit on the
    cluster's header; the results land on the **Users** and **Databases**
    tabs.

    The generated password is shown once, right after the user is created.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters/{cluster_id}/users      { "name": "app_user", "permissions": {...} }
    POST /v1/clusters/{cluster_id}/databases  { "name": "orders" }
    ```
  </Tab>
</Tabs>

Both answer **`202`**. The API records what you asked for and the cluster
converges on it moments later, so a role or database is not necessarily usable
the instant the call returns — check for it on the cluster before pointing an
application at it.

`permissions` is an engine-specific grant map carried through to the cluster.

### Reserved names

Names must be postgres-safe identifiers — a letter or underscore, then letters,
digits and underscores, up to 64 characters. On top of that:

| Refused as a user                                        | Refused as a database                |
| -------------------------------------------------------- | ------------------------------------ |
| `postgres`, `replicator`, `default`, `basaltic-sentinel` | `postgres`, `template0`, `template1` |
| anything starting `pg_`                                  |                                      |
| the cluster's own `admin_user`                           |                                      |

The platform-managed principals are excluded because the cluster's own
replication, bootstrap and coordination depend on them; renaming or dropping
one from underneath would break the cluster rather than the application.

### Rotating a password

<Tabs>
  <Tab title="Console">
    **Rotate password** on the user's row, on the cluster's **Users** tab,
    confirmed under the same name. The new password is shown once; the
    **Password secret** column links to where it is kept.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters/{cluster_id}/users/{user_id}/rotate-password
    ```
  </Tab>
</Tabs>

Issues a fresh password, writes it to a new secret, and returns it **once**.
The cluster's own `admin_user` cannot be rotated through this path — its secret
belongs to the bootstrap path.

## Backups and restore

Every cluster gets a backup repository at create. Postgres additionally
archives its write-ahead log continuously, which is what makes
point-in-time recovery possible between full backups.

### The schedule

|            |                                                                           |
| ---------- | ------------------------------------------------------------------------- |
| When       | Daily at 03:00 UTC, with up to 15 minutes of jitter                       |
| What       | A full backup on Sundays, an incremental on other days                    |
| Where from | The primary only — replicas never run the backup                          |
| Retention  | The two most recent **full** backups, plus the WAL needed to recover them |

With a weekly full and two kept, the recoverable window reaches back roughly
two weekly cycles.

<Warning>
  Retention is enforced in the backup repository, not in the catalogue. A row
  in `GET /v1/clusters/{cluster_id}/backups` is a record of a backup that ran —
  it is not by itself proof that the data is still inside the retention window.
</Warning>

### Taking one yourself

<Tabs>
  <Tab title="Console">
    **Backup now** on the cluster opens **Request backup**. **Kind** offers
    **Base — complete backup** or **Incremental — changes since the last
    backup**; **Request backup** submits it. The button is disabled unless the
    cluster is active.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters/{cluster_id}/backups
    { "kind": "base" }
    ```
  </Tab>
</Tabs>

`kind` is `base` (the default — a full copy, needing no earlier backup to build
on) or `incremental` (only what changed since the last one). `full` and `incr`
are accepted as aliases on both the request and the list filter, but the backup
is stored and returned as `base` or `incremental`.

The cluster must be `active` or `building`. A backup that never reports within
six hours is marked `failed`.

<Note>
  Valkey takes full backups only — its backup path is a complete dump every
  time, so `incremental` is refused for that engine.
</Note>

### Restoring in place

<Tabs>
  <Tab title="Console">
    On the cluster's **Backups** tab, **Restore here** on the backup's row
    opens **Restore into this cluster**. It asks you to **Type the cluster
    name to confirm** before the button does anything.

    The action only appears on a postgres backup that has succeeded, and only
    while the cluster is active.

    <Note>
      **`recovery_target_time` is API only here.** The in-place dialog restores
      to the backup as taken; point-in-time recovery is offered only when
      restoring into a *new* cluster. Send the call below if you need to stop
      at a particular instant in this cluster.
    </Note>
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters/{cluster_id}/restore
    {
      "backup_id": "3c4d5e6f-7081-4293-a4b5-c6d7e8f9a0b1",
      "recovery_target_time": "2026-01-15T08:00:00Z",
      "confirm": "prod-orders-db"
    }
    ```
  </Tab>
</Tabs>

This overwrites **this** cluster's data and keeps everything else: the same
endpoint, security groups, IAM role and parameter group. It is the "undo a bad
migration" path — the restore was never the hard part, the cutover was.

<Warning>
  Destructive and irreversible. `confirm` must equal the cluster's name, so
  consent cannot arrive by accident from a retried request, a stale tab, or a
  script looping over ids.
</Warning>

Before anything is overwritten, a **pre-restore backup is requested
automatically** — if that backup cannot be started, the restore does not
happen. That is the way back from a mistaken restore.

The call answers `202` and the cluster reports `restoring`, which is neither
`active` nor `building`: while it holds, the cluster must not be read as
serving current data. An HA cluster is restored as a whole — the leader is
restored and the replicas are rebuilt from it. A restore that fails leaves the
cluster serving, with a `fault` recorded and the pre-restore backup still
available.

`recovery_target_time` (RFC 3339) replays the log forward from the backup to
that instant and stops there. Omit it to recover to the latest archived point.
A target earlier than the backup's `earliest_restore_at` is refused.

The source backup may belong to this cluster or to **another cluster in the
same account**; a cross-cluster restore grants this cluster read on that
repository for the duration and revokes it afterwards.

Postgres only. The cluster must be `active`, and a second restore while one is
in flight is a `409`.

### Restoring into a new cluster

Pass `restore_from` on create to bootstrap a **new** cluster from a backup
instead of an empty database:

<Tabs>
  <Tab title="Console">
    On **Create cluster**, tick **Restore from an existing backup** under
    **Restore from backup**, then pick the **Source cluster** and the
    **Backup**. **Recovery target time** is optional — leave it blank to
    restore to the latest available point.

    The shortcut is **To new cluster**, on a backup's row on the source
    cluster's **Backups** tab: it opens the same form with the source and
    backup already filled in.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/clusters
    {
      "name": "orders-db-pg18",
      "engine_type": "postgres",
      "engine_version": "18",
      "flavor_id": "...",
      "storage_gb": 200,
      "networks": [{ "subnet_id": "..." }],
      "restore_from": {
        "backup_id": "3c4d5e6f-7081-4293-a4b5-c6d7e8f9a0b1",
        "recovery_target_time": "2026-01-15T08:00:00Z"
      }
    }
    ```
  </Tab>
</Tabs>

This is the route around every immutable field: a bigger disk, a different
flavor, a newer major version. Create the new cluster from a backup, verify it,
then move your connection strings. HA works here too — only the bootstrap
primary is seeded from the backup, and the replicas rebuild from it.

The source backup must belong to the same account and be `succeeded`. Valkey
does not support it.

### Choosing a backup

`GET /v1/clusters/{cluster_id}/backups` lists what ran. Two fields decide
whether a given row can be restored:

<ResponseField name="status" type="must be succeeded">
  `running` and `failed` rows are not restore targets.
</ResponseField>

<ResponseField name="restorable" type="boolean">
  `true` means the backup can be selected **by name**. `false` means it predates
  the platform recording backup labels, so it can only be restored while it is
  the source cluster's most recent succeeded backup — honouring it otherwise
  would silently restore a different backup than the one you picked.
</ResponseField>

<Warning>
  **Deleting a cluster deletes its backup catalogue.** The stored data is held
  for a recovery window before it is purged, but the rows the API restores from
  are gone with the cluster, so there is no self-service path back. Take a
  backup and restore it into a second cluster *before* you delete the first.
</Warning>

Once you have that, the delete itself:

<Tabs>
  <Tab title="Console">
    **Delete cluster**, on the cluster's **Settings** tab. You are asked to
    type the cluster's name to confirm.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    DELETE /v1/clusters/{cluster_id}
    ```
  </Tab>
</Tabs>

## Parameter groups

A parameter group is a named set of engine settings that clusters bind to.
Bound clusters converge on the group's settings, so editing the group reaches
every cluster using it.

<Tabs>
  <Tab title="Console">
    Go to **Databases → Parameter groups** and choose **Create parameter
    group**. **Details** takes the **Name**, an optional **Description**, the
    **Engine** and the **Engine version**. **Parameters** is where the
    settings go, one **Add parameter** at a time.

    A **Tunable parameters** card on the same page lists what this engine
    accepts, so you can pick from it instead of guessing.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST /v1/parameter-groups
    {
      "name": "write-heavy-pg17",
      "engine_type": "postgres",
      "engine_version": "17",
      "params": { "work_mem": "64MB", "max_connections": "300" }
    }
    ```
  </Tab>
</Tabs>

Bind it at create with `parameter_group_id`, or later with
`PATCH /v1/clusters/{cluster_id}`. An empty string on the patch clears the
binding and returns the cluster to engine defaults.

<Note>
  The console binds a group **at create only** — the **Parameter group** field
  on **Create cluster**, which lists only groups matching the engine and
  version you picked. Rebinding or unbinding a cluster afterwards is **API
  only**: the cluster's **Settings** tab edits the description and tags, and
  the **Parameter group** shown on its overview is a link, not a control.
</Note>

<Note>
  A group must match the cluster's engine type **and major version exactly**. A
  postgres 17 group on a postgres 18 cluster is not a near miss — settings are
  version-specific, and binding across versions would apply parameters the
  engine may not have.
</Note>

### What you can tune

```bash theme={null}
GET /v1/engines/{engine_type}/parameters
```

That endpoint is the allowlist: every tunable setting, its value grammar, its
bounds, and whether applying it needs a restart. Read it rather than
discovering the list by collecting `400`s — a group naming an unlisted setting
is rejected outright, not quietly dropped. It is the same list the console
shows as **Tunable parameters** while you edit a group.

Settings the platform owns are absent and refused, and the reasons are worth
knowing:

* **`wal_level`, `archive_mode`, `archive_command`, `max_wal_senders`** —
  backups and replication are built on these.
* **`listen_addresses`, `port`, `data_directory`, `unix_socket_directories`** —
  provisioning assumes them.
* **`hot_standby`, `primary_conninfo`, `synchronous_standby_names`,
  `recovery_target*`** — owned by the HA supervisor.
* **`shared_preload_libraries`** — a bad value stops the server booting at all.

On valkey the same logic excludes `bind`, `port` and `tls-*` (the listener),
`requirepass`, `aclfile` and `user` (how the managed user model authenticates),
and `dir`, `dbfilename` and `appendfilename` (where the backup path looks).

Values are validated against the grammar and bounds: `256MB` and `30s` are
accepted with units, and bounds are compared in the base unit, so `1GB` and
`1024MB` are the same value. A rejection names the offending key.

### When a change takes effect

Each parameter reports an `apply` value:

| `apply`   | What happens                                              |
| --------- | --------------------------------------------------------- |
| `reload`  | Lands on the running engine without dropping connections. |
| `restart` | Stored now, in effect at the engine's **next restart**.   |

<Warning>
  The platform will not bounce a cluster to apply a config edit. Set a
  `restart` parameter — `max_connections` and `shared_buffers` are the common
  ones — and the cluster keeps running on the old value until it restarts for
  some other reason. Check `apply` before you plan a change around it.
</Warning>

### Editing and deleting

<Tabs>
  <Tab title="Console">
    Open the group and edit its **Parameters** card, adding rows with **Add
    parameter**. Saving replaces the full set, exactly as the API call does.

    A group whose **Managed by** reads **Platform** is read-only — copy its
    values into a group of your own to change anything.

    **Delete parameter group** sits on the group's page.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    PATCH /v1/parameter-groups/{parameter_group_id}
    { "params": { "work_mem": "64MB", "max_connections": "300" } }
    ```
  </Tab>
</Tabs>

`params` **replaces the whole set**, and republishes it to every bound cluster.

<Warning>
  A setting you drop from the group is not left behind on the clusters — they
  converge on the new set, so it reverts to the engine default. Send the
  complete map you want, not just the keys you are changing.
</Warning>

A group that is still bound to a cluster cannot be deleted; the `409` names the
clusters holding it. Move them off first.

## Access control

Every operation checks an IAM policy. Cluster and backup actions authorize
against the cluster's CRN, parameter-group actions against the group's:

```
crn:database:sa-saopaulo-1:<account>:cluster/<cluster_id>
crn:database:sa-saopaulo-1:<account>:parameter-group/<parameter_group_id>
```

<Note>
  These CRNs are keyed by **id**, not by name, so you cannot fence a naming
  convention the way you can for a name-based resource. Fence with tags
  instead — see below and [Policies](/iam/policies).
</Note>

| Area             | Actions                                                                                                            |
| ---------------- | ------------------------------------------------------------------------------------------------------------------ |
| Clusters         | `CreateCluster`, `GetCluster`, `ListClusters`, `UpdateCluster`, `DeleteCluster`                                    |
| Topology         | `ConvertToHA`, `AddReplica`, `RemoveReplica`, `Failover`                                                           |
| Backups          | `CreateBackup`, `ListBackups`, `GetBackup`, `RestoreCluster`                                                       |
| Users            | `CreateDBUser`, `GetDBUser`, `ListDBUsers`, `DeleteDBUser`, `RotateDBUserPassword`                                 |
| Databases        | `CreateDatabase`, `GetDatabase`, `ListDatabases`, `DeleteDatabase`                                                 |
| Parameter groups | `CreateParameterGroup`, `GetParameterGroup`, `ListParameterGroups`, `UpdateParameterGroup`, `DeleteParameterGroup` |

<Note>
  Requesting a backup needs **`database:CreateBackup`**, even though the
  operation is `requestBackup` and the audit trail records it as
  `database:RequestBackup`. Grant the action, not the verb in the path.
</Note>

Cluster and parameter-group actions carry tag context, so
`basalt:ResourceTag/<key>` conditions work on them, and
`basalt:RequestTag/<key>` fences what a create or update may label a resource
as.

<Warning>
  The **user and logical-database actions carry no tag context.** An `allow`
  conditioned on `basalt:ResourceTag/...` fails closed on
  `database:CreateDBUser`, `database:CreateDatabase` and their siblings,
  because the key is absent from the request — so a tag-scoped grant that works
  for the cluster will deny managing users inside it. Grant those actions
  without a tag condition, or scope them by CRN.
</Warning>

Reads of the whole collection — `ListClusters`, `ListParameterGroups` — are
authorized against the account-wide CRN (`cluster/*`), so a policy can grant
listing in one account or region and not another.

## Cluster statuses

```mermaid theme={null}
stateDiagram-v2
    state "failing-over" as failing_over
    [*] --> pending: create
    pending --> building: members provisioned
    building --> active: every member reports ready
    active --> converting: convert-to-ha
    active --> restoring: restore
    active --> failing_over: failover
    active --> modifying: day-2 change
    converting --> active: reported
    restoring --> active: reported
    failing_over --> active: reported
    modifying --> active
    active --> deleting: delete
    deleting --> deleted
    building --> error: provisioning failed
```

| Status                 | Meaning                                                                                                             |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `pending`              | The row exists; members are not requested yet.                                                                      |
| `building`             | Members are provisioning and booting.                                                                               |
| `active`               | Serving. The only state most day-2 operations accept.                                                               |
| `modifying`            | A day-2 change is in progress.                                                                                      |
| `converting`           | A single node is being adopted into HA. Replica adds, failovers and further conversions are refused while it holds. |
| `restoring`            | Data is being overwritten in place. **Not serving current data.**                                                   |
| `failing-over`         | A switchover is pending or in flight.                                                                               |
| `deleting` / `deleted` | Teardown.                                                                                                           |
| `error`                | Terminal failure — read `fault` for the code and message.                                                           |

## Troubleshooting

<AccordionGroup>
  <Accordion title="The cluster is stuck in building" icon="clock">
    A cluster reaches `active` only when every member reports readiness, and
    first boot installs and initialises the engine from scratch. Minutes are
    normal. If it has not moved much longer than that, read `fault` on the
    cluster and check `instances[].status` — a member that never reached
    `running` is a provisioning problem, not a database one.
  </Accordion>

  <Accordion title="Add-replica is refused on a cluster that looks fine" icon="git-branch">
    Read `patroni_managed`, not `instance_count`. A cluster created
    single-node has no HA machinery for a replica to join and is refused until
    you run `POST /v1/clusters/{cluster_id}/convert-to-ha`. After a successful
    conversion the same request works unchanged — even though the cluster
    still has exactly one member.
  </Accordion>

  <Accordion title="The failover went back to active without switching" icon="rotate-cw">
    A pending switchover has to be picked up and executed by the current
    leader. One that is not executed within 10 minutes is cleared and the
    cluster is rolled back to `active` so it is not stuck in `failing-over`.
    Retry it. If it keeps expiring, check that the current primary is healthy
    in `instances`.
  </Accordion>

  <Accordion title="A parameter group edit changed nothing" icon="sliders">
    Check the parameter's `apply` in
    `GET /v1/engines/{engine_type}/parameters`. A `restart` parameter is stored
    immediately and takes effect only at the engine's next restart — the
    platform does not restart a cluster to apply a config edit.

    Also confirm the cluster is actually bound: `parameter_group_id` on the
    cluster has to name the group you edited.
  </Accordion>

  <Accordion title="A restore is refused" icon="triangle-alert">
    Three checks, in order. `confirm` must equal the cluster's name exactly.
    The cluster must be `active` — a cluster in `restoring`, `converting` or
    `error` will not accept one. And the source backup must be `succeeded`
    with `restorable: true`, or else be the source cluster's most recent
    succeeded backup.
  </Accordion>

  <Accordion title="Nothing can reach the database" icon="plug">
    Work outward. Confirm you are connecting to `endpoints[].dns_name` and the
    endpoint's `port` — 5432 for postgres, 6379 for valkey. Confirm the
    exposure matches where you are calling from: `assign_public_ip: false`
    means the endpoint only answers inside the VPC. Then check that a security
    group admitting your source is attached to the members' interfaces —
    rules that exist but are not on the interface do nothing. See
    [Networking](/networking).
  </Accordion>

  <Accordion title="A user or database I created is not there yet" icon="users">
    Both calls answer `202`. They record intent, and the cluster converges on
    it a moment later. Poll the cluster's users or databases before pointing an
    application at one.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Networking" icon="network" href="/networking">
    Subnets, security groups and the internet gateway a public cluster needs.
  </Card>

  <Card title="Policies" icon="shield" href="/iam/policies">
    Writing the conditions that fence who may touch which cluster.
  </Card>

  <Card title="API reference" icon="code" href="/api-reference/introduction">
    Every cluster, user, backup and parameter-group operation, with schemas.
  </Card>

  <Card title="Support" icon="life-buoy" href="/support">
    When a cluster is in `error` and `fault` does not explain it.
  </Card>
</CardGroup>
