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

# Convert a single-node postgres cluster to Patroni-managed HA

> Turns a single-node postgres cluster into a Patroni-managed one IN PLACE,
keeping its endpoint and its data — the path from the cheap first choice
to HA without dumping and recreating.

**This restarts postgres.** A cluster created single-node runs plain
postgres; converting installs Patroni, provisions the replication user and
the internal security group, and brings the node back up under Patroni
against its existing data directory. There is no replica to fall back on
while that happens, so the restart is on the only copy of the data.

Asynchronous. The cluster moves to `converting` and returns to `active`
when the member reports the outcome; it becomes `patroni_managed` only on
success. A conversion that fails leaves the node serving plain postgres,
which is the state it was already in. Poll the cluster to follow it.

Postgres only — valkey self-clusters through Sentinel and needs at least
three members, so its conversion is a different shape.

Afterwards, add replicas with POST /v1/clusters/{cluster_id}/replicas.




## OpenAPI

````yaml /api-reference/specs/database.yaml post /v1/clusters/{cluster_id}/convert-to-ha
openapi: 3.0.3
info:
  title: Basaltic Database API
  version: 1.0.0
  description: |
    Managed database clusters — provision an engine, convert a single node to
    high availability, fail over between members and restore from a
    backup.
  contact:
    name: Basaltic Support
    email: ping@basaltic.sh
  license:
    name: Proprietary
    url: https://basaltic.sh/terms
servers:
  - url: https://database.{region}.basaltic.sh
    description: Regional API endpoint
    variables:
      region:
        default: sa-saopaulo-1
        description: Region code
security:
  - SignatureAuth: []
paths:
  /v1/clusters/{cluster_id}/convert-to-ha:
    post:
      tags:
        - Database
      summary: Convert a single-node postgres cluster to Patroni-managed HA
      description: >
        Turns a single-node postgres cluster into a Patroni-managed one IN
        PLACE,

        keeping its endpoint and its data — the path from the cheap first choice

        to HA without dumping and recreating.


        **This restarts postgres.** A cluster created single-node runs plain

        postgres; converting installs Patroni, provisions the replication user
        and

        the internal security group, and brings the node back up under Patroni

        against its existing data directory. There is no replica to fall back on

        while that happens, so the restart is on the only copy of the data.


        Asynchronous. The cluster moves to `converting` and returns to `active`

        when the member reports the outcome; it becomes `patroni_managed` only
        on

        success. A conversion that fails leaves the node serving plain postgres,

        which is the state it was already in. Poll the cluster to follow it.


        Postgres only — valkey self-clusters through Sentinel and needs at least

        three members, so its conversion is a different shape.


        Afterwards, add replicas with POST /v1/clusters/{cluster_id}/replicas.
      operationId: convertClusterToHA
      parameters:
        - name: cluster_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: Conversion accepted; the member has not restarted yet.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClusterResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: >
            Already Patroni-managed, a conversion is in flight, the cluster is
            not active, or it does not have exactly one member.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    ClusterResponse:
      type: object
      properties:
        cluster:
          $ref: '#/components/schemas/Cluster'
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - request_id
          properties:
            code:
              type: string
              description: Error code identifying the type of error
              example: INVALID_INPUT
            message:
              type: string
              description: Human-readable error message
              example: Invalid request parameters
            request_id:
              type: string
              format: uuid
              description: Request ID for debugging
              example: 550e8400-e29b-41d4-a716-446655440000
    Cluster:
      type: object
      required:
        - id
        - crn
        - name
        - engine_type
        - engine_version
        - flavor_id
        - storage_gb
        - instance_count
        - assign_public_ip
        - status
        - tags
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
          example: 9b2e4c7a-1f3d-4a8e-bc25-6d0f1a2b3c4d
        crn:
          type: string
          description: IAM resource CRN
          example: >-
            crn:database:sa-saopaulo-1:my-account:cluster/9b2e4c7a-1f3d-4a8e-bc25-6d0f1a2b3c4d
        name:
          type: string
          example: prod-orders-db
        description:
          type: string
          example: Primary orders database
        engine_type:
          type: string
          enum:
            - postgres
            - valkey
          example: postgres
        engine_version:
          type: string
          description: e.g. '17' for postgres
          example: '17'
        flavor_id:
          type: string
          format: uuid
          description: >-
            Compute flavor each cluster member runs on. Must be a
            database-family flavor.
          example: e5f6a7b8-c9d0-4123-e4f5-a6b7c8d9e0f1
        storage_gb:
          type: integer
          minimum: 1
          example: 100
        instance_count:
          type: integer
          minimum: 1
          description: >
            Node count. Postgres: 1 = single-node; 2+ adds Patroni HA members
            (max 10). Valkey: 1 = single-node; 3 = primary + replicas with
            Sentinel (2 is rejected; max 3). Does not guarantee a completed
            failover drill for every topology.
          example: 1
        assign_public_ip:
          type: boolean
          description: >
            Whether the cluster's endpoints are backed by floating IPs and
            reachable from the internet. False means every endpoint resolves to
            a member's in-VPC address and the cluster is reachable only from the
            VPC.
          example: true
        patroni_managed:
          type: boolean
          description: >
            Whether postgres here runs under Patroni. Not the same question as
            instance_count >= 2: a cluster converted from single-node is
            Patroni-managed with one member — able to take a replica, not yet
            HA. Adding a replica requires this, not the node count.
          example: false
        parameter_group_id:
          type: string
          format: uuid
          example: 1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9
        status:
          type: string
          description: >
            Lifecycle status of the cluster. `converting` is a single-node
            postgres cluster being adopted into Patroni — the node restarts
            under a new supervisor, so replica adds, failovers and further
            conversions are refused while it holds. `restoring` is a cluster
            whose data is being overwritten in place: it is neither active nor
            building, and must not be read as serving current data.
          enum:
            - pending
            - building
            - active
            - modifying
            - converting
            - restoring
            - failing-over
            - deleting
            - deleted
            - error
          example: active
        fault:
          $ref: '#/components/schemas/Fault'
        endpoints:
          type: array
          items:
            $ref: '#/components/schemas/Endpoint'
        instances:
          type: array
          items:
            $ref: '#/components/schemas/Instance'
        admin_user:
          type: string
          description: Bootstrap admin/superuser role name.
          pattern: ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$
          example: admin
        admin_secret_id:
          type: string
          format: uuid
          description: secrets service id holding the admin password.
          example: 2b3c4d5e-6f70-4182-93a4-b5c6d7e8f9a0
        default_database:
          type: string
          pattern: ^[a-zA-Z_][a-zA-Z0-9_]{0,63}$
          example: default
        metadata:
          $ref: '#/components/schemas/Metadata'
        tags:
          $ref: '#/components/schemas/Tags'
        created_at:
          type: string
          format: date-time
          example: '2026-01-15T09:30:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-01-15T09:30:00Z'
        deleted_at:
          type: string
          format: date-time
          example: '2026-01-16T09:30:00Z'
    Fault:
      type: object
      description: Error detail attached to a resource in status=error.
      required:
        - code
        - message
        - at
      properties:
        code:
          type: string
          example: provision_failed
        message:
          type: string
          example: compute instance failed to boot
        details:
          type: string
          example: quota exceeded in subnet
        at:
          type: string
          format: date-time
          example: '2026-01-15T09:31:00Z'
    Endpoint:
      type: object
      description: One writer/reader/admin connection endpoint for a cluster.
      required:
        - id
        - kind
        - dns_name
        - port
      properties:
        id:
          type: string
          format: uuid
          example: a7b8c9d0-e1f2-4345-a6b7-c8d9e0f1a2b3
        kind:
          type: string
          description: Which role this endpoint targets.
          enum:
            - writer
            - reader
            - admin
          example: writer
        floating_ip_id:
          type: string
          format: uuid
          description: >
            The endpoint's public floating IP. Absent on clusters created with
            assign_public_ip=false, whose endpoints resolve to a member's in-VPC
            address instead. Connect by dns_name in both cases.
          example: f6a7b8c9-d0e1-4234-f5a6-b7c8d9e0f1a2
        ip_address:
          type: string
          description: >
            The address `dns_name` currently answers with — the floating IP on a
            public endpoint, or the serving member's in-VPC address on a private
            one. Absent while it cannot be resolved, e.g. an endpoint whose
            member is still provisioning. It is resolved per request and a
            private endpoint's changes on failover, so connect by `dns_name` and
            use this for the things a name cannot express, such as a
            security-group rule.
          example: 203.0.113.50
        dns_name:
          type: string
          example: orders-db.my-account.db.sa-saopaulo-1.basaltic.sh
        port:
          type: integer
          minimum: 1
          maximum: 65535
          example: 5432
    Instance:
      type: object
      description: One VM-shaped member of a cluster.
      required:
        - id
        - role
        - status
      properties:
        id:
          type: string
          format: uuid
          example: b8c9d0e1-f2a3-4456-b7c8-d9e0f1a2b3c4
        member_name:
          type: string
          description: >-
            Cluster member name — pass as failover target_member to promote this
            node.
          example: dbaas-b8c9d0e1-0
        compute_instance_id:
          type: string
          format: uuid
          example: c9d0e1f2-a3b4-4567-c8d9-e0f1a2b3c4d5
        boot_volume_id:
          type: string
          format: uuid
          example: d0e1f2a3-b4c5-4678-d9e0-f1a2b3c4d5e6
        role:
          type: string
          description: Member role within the cluster.
          enum:
            - primary
            - replica
          example: primary
        status:
          type: string
          example: running
        agent_version:
          type: string
          description: >
            Version of the in-VM management agent this member reported in its
            last heartbeat. Absent when the member has not reported one — it has
            not beat yet, or it runs an agent released before the field existed.
            Absent means unknown: never assume it matches another member's.
          example: 1.7.12-1
    Metadata:
      type: object
      additionalProperties:
        type: string
      example:
        environment: production
        team: backend
    Tags:
      type: object
      additionalProperties:
        type: string
      example:
        environment: production
        team: backend
  responses:
    BadRequest:
      description: Invalid request parameters
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INVALID_INPUT
              message: Invalid request parameters
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHORIZED
              message: Authentication required
              request_id: 550e8400-e29b-41d4-a716-446655440000
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: ACCESS_DENIED
              message: You don't have permission to perform this action
              request_id: 550e8400-e29b-41d4-a716-446655440000
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: Resource not found
              request_id: 550e8400-e29b-41d4-a716-446655440000
  securitySchemes:
    SignatureAuth:
      type: apiKey
      in: header
      name: Authorization
      description: >
        Request signing with an access key issued to a service account. An

        HMAC-SHA256 over a canonical form of the request, close to AWS SigV4.

        The `basaltic` CLI signs for you.


        Send `Authorization`, `X-Date` (UTC, `YYYYMMDDTHHMMSSZ`) and `X-Nonce`

        (random per request); add `X-Content-Sha256` to bind a body, and

        `X-Amz-Security-Token` when using temporary credentials.


        ```

        Authorization: BASALTIC-HMAC-SHA256
        Credential=<access_key_id>/<date>/<region>/basaltic/basaltic_request,
        SignedHeaders=host;x-date;x-nonce, Signature=<hex>

        ```


        `<region>` is the region code you are calling, or `global` for the
        global

        services. A signature is valid for 5 minutes from `X-Date`, and mutating

        requests are replay-guarded on the nonce.


        **Full signing procedure, including a working implementation:**

        https://docs.basaltic.sh/authentication


        ## Rate limits

        There is no global request budget. A limit applies only where an

        operation documents a `429`, and that operation says what it counts.

        Those responses carry `X-RateLimit-Limit`, `X-RateLimit-Remaining`,

        `X-RateLimit-Reset` and, on a `429`, `Retry-After` — read them rather

        than hard-coding a number. Retrying before `Retry-After` is refused and

        extends the window. Everything else is bounded by quota, not by request

        rate.

````