External Database
Provision external PostgreSQL and a least-privilege role for an AISIX Enterprise control plane installed with Helm, without granting superuser access.
Applies to:AISIX AI Gateway 1.4.0
Deployments:KubernetesRoles:Platform engineerSecurity engineerTime:20 minutesImpact:Changes live traffic
Goal
An AISIX Enterprise control plane running on a PostgreSQL database your DBA team manages, reached by one login role that has neither SUPERUSER nor any PostgreSQL extension.
The control plane restarts while it picks the database up, and gateways keep serving traffic from their cached configuration while it does — so the badge above over-warns for a first installation. It is the truthful badge for the case this page also covers: pointing an in-service control plane at a fresh database replaces the system of record, and what the gateways are served next is what the new database holds. PostgreSQL is the only supported engine.
Docker Compose requires bundled PostgreSQL
The online and offline Docker Compose packages do not support an external database. Their Compose files set the control-plane services' database URLs to the bundled postgres service and do not expose overrides in .env. Use Helm when the control plane must connect to an external PostgreSQL database.
Three control-plane services share the one database:
cp-api: the control-plane API server. It runs the schema migration on startup.dp-manager: the data-plane manager. It connects to the same database but does not run the control-plane application migration; its embedded Kine backend manages its own storage schema. On a fresh database it retries missing application-schema errors for about two minutes whilecp-apicompletes the migration.- The dashboard: the control-plane user interface. It stores authentication data in the same database.
Before you begin
-
You can reach the database server as an administrator that can create roles and databases and grant
BYPASSRLS, and the server is PostgreSQL 14 or later. Check it with:psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d postgres -tAc "SHOW server_version;"Expected: a version of
14or later. -
The administrator you are connecting as can grant the attributes the connection role needs. Check it with:
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d postgres -tAc \ "SELECT rolsuper OR (rolcreaterole AND rolcreatedb AND rolbypassrls) FROM pg_roles WHERE rolname = current_user;"Expected:
t. A role that can create roles and databases but cannot grantBYPASSRLSgets as far as step 2 and fails there. -
You are installing with Helm. Check it with:
helm version --shortExpected: a
v3.version line. The Compose packages cannot use an external database at all, for the reason in the callout above. -
You have chosen a URL-safe password. The chart builds a
postgres://connection URL from the credentials, so+,/, and=break the URL unless they are percent-encoded. Generate one with:openssl rand -hex 24Expected: 48 hexadecimal characters.
Set the variables this guide uses:
export AISIX_URL="http://127.0.0.1:8080" # the control plane's origin; the AISIX Enterprise Admin API is served under /api
export AISIX_DB_HOST="postgres.example.internal"
export AISIX_DB_ADMIN="postgres" # the administrator you connect as, not the control plane's role
export AISIX_DB_ROLE="aisix" # the login role the control plane will use
export AISIX_DB_NAME="aisix_cloud" # the database it will own
export AISIX_CP_RELEASE="aisix-cp" # the control-plane Helm release
export AISIX_NAMESPACE="aisix-cp" # the namespace it is installed in
read -rsp "New database role password: " AISIX_DB_PASSWORD && export AISIX_DB_PASSWORD && echoThe password is read rather than written into a command you keep. On a shared host, set it with psql's own \password prompt after step 1 instead of passing it on the command line at all.
Steps
Four steps: the role, the database it owns, the schema grant a hardened public needs, and the Helm values that point the control plane at both. Run steps 1 to 3 once, as a database administrator.
Step 1: Create the login role
The control plane connects as this role. It is not a superuser, and it is not cp_api_app — that one the control plane creates itself.
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d postgres -c \
"CREATE ROLE ${AISIX_DB_ROLE} LOGIN PASSWORD '${AISIX_DB_PASSWORD}' NOSUPERUSER CREATEROLE BYPASSRLS;"CREATE ROLEWhat changed: the role exists and can sign in; it owns nothing yet.
Why these attributes, and why not SUPERUSER
On its first start, cp-api prepares the database schema and creates an internal low-privilege role. Each attribute pays for one part of that bootstrap:
- Database ownership (step 2) lets
cp-apicreate thepublicandauthschemas, tables, indexes, functions, and Row-Level Security policies. Under default PostgreSQL privileges the database owner can create objects inpublic, so a standard cluster needs no separate schema grant. CREATEROLEletscp-apicreate and configurecp_api_app, the internal role that runs tenant-scoped queries under Row-Level Security.BYPASSRLSlets shared control-plane paths read rows across organizations where they have to: caller-token authentication, billing webhooks, the background budget aggregator, anddp-manager's own operations.
Tenant-scoped requests still switch to cp_api_app, which has neither SUPERUSER nor BYPASSRLS. So the connection role needs specific PostgreSQL capabilities and no superuser privilege.
Use this table when a DBA team reviews the role:
| Requirement | Purpose |
|---|---|
LOGIN | Allows control-plane services to connect as this role. |
| Database ownership | Allows cp-api to create and alter schemas, tables, indexes, functions, and RLS policies. |
CREATEROLE | Allows cp-api to create and manage the internal cp_api_app role. |
BYPASSRLS | Allows cross-organization control-plane paths and dp-manager to read the rows they need on the shared connection. |
Not SUPERUSER | Keeps the role below PostgreSQL superuser privilege. |
Step 2: Create the database it owns
One empty database, owned by the role from step 1. Ownership is what lets the role create objects in public and add the auth schema on first boot.
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d postgres -c \
"CREATE DATABASE ${AISIX_DB_NAME} OWNER ${AISIX_DB_ROLE};"CREATE DATABASEDo not pre-create the cp_api_app role. The control plane creates and configures it during migration and asserts its attributes; one that already exists, especially with different attributes, can make the migration fail its own safety checks.
What changed: an empty database exists with the control plane's role as its owner. No AISIX table exists yet.
Step 3: Grant schema access, if public is hardened
Skip this step on a standard cluster: the owner can already create objects in public. Run it when your DBA team has revoked the default CREATE privilege on that schema.
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d "${AISIX_DB_NAME}" -c \
"GRANT USAGE, CREATE ON SCHEMA public TO ${AISIX_DB_ROLE};"GRANTWhat changed: the role can create the objects step 4 sets off, on a cluster where ownership alone was not enough.
Step 4: Point the control plane at the database
Turn the bundled PostgreSQL off and give the chart the external database's values:
postgresql:
builtin: falseThe externalDatabase.* values carry the host, database, and credentials, and the chart builds each control-plane service's connection URL from them. The value list for the chart version you are installing is in On-Premises Configuration, and helm show values api7/aisix-cp prints the exact key names. Set sslmode=require, or stricter such as verify-full, whenever the database is reached over a network you do not fully control.
Then apply the values and wait for cp-api, which is the service that runs the migration:
helm upgrade --install "${AISIX_CP_RELEASE}" api7/aisix-cp \
-n "${AISIX_NAMESPACE}" --create-namespace -f your-cp-values.yaml
kubectl -n "${AISIX_NAMESPACE}" rollout status deployment \
-l app.kubernetes.io/instance="${AISIX_CP_RELEASE}",app.kubernetes.io/component=apiWhat changed: the control plane is running on your database, and the schema exists in it.
A private database CA needs one more value
The dashboard's Node.js client keeps its own trust store. If external PostgreSQL presents a certificate from a private CA, registration and sign-in can fail with SELF_SIGNED_CERT_IN_CHAIN while dashboard pages themselves load. Mount the CA and set NODE_EXTRA_CA_CERTS — the values and the kubectl create configmap command are in On-Premises Configuration.
What the control plane creates on first start
cp-api builds the full schema on the first successful start, and on later starts applies pending migrations only:
- the
publicschema for control-plane tables — organizations, environments, models, caller API keys, budgets — owned by your role; - the
authschema for authentication tables, such as users and sessions; - the
cp_api_approle, withNOSUPERUSER NOBYPASSRLS NOINHERIT NOLOGIN, granted only CRUD on control-plane tables and read and write on a few non-secret identity columns; - Row-Level Security policies that scope each tenant table to one organization.
Your connection role is made a member of cp_api_app automatically, so it can switch to that role per request. You do not grant that yourself. Two queries show that the bootstrap landed as described — the internal role exists and is de-privileged:
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d "${AISIX_DB_NAME}" -c \
"SELECT rolname, rolsuper, rolbypassrls, rolcanlogin FROM pg_roles WHERE rolname = 'cp_api_app';"cp_api_app | f | f | fand both schemas were created:
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d "${AISIX_DB_NAME}" -tAc \
"SELECT nspname FROM pg_namespace WHERE nspname IN ('public', 'auth');"public
authValidate
Ask the control plane itself, from outside the cluster, rather than asking the database. It answers this endpoint only after cp-api has started, and cp-api starts only once its migration against your database has succeeded:
curl -sS "${AISIX_URL}/api/config/public" | jq '{cp_version, cp_commit}'{ "cp_version": "1.0.0", "cp_commit": "a22f131" }cp_version is empty on a build that was not cut from a release, and cp_commit identifies the commit either way. The dashboard shows the same identity at the bottom of its left navigation.
Roll back / Clean up
Going back to the chart's bundled PostgreSQL is the reverse of step 4 — set postgresql.builtin: true, remove the externalDatabase.* values, and upgrade the release again. Nothing carries the data across: the bundled instance starts empty and cp-api migrates it from scratch, so a control plane that has been in use needs a database backup and restore instead of a values change.
When the external database was a trial and is not the one you are keeping, remove what steps 1 and 2 created, in that order:
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d postgres -c \
"DROP DATABASE ${AISIX_DB_NAME};"
psql -h "${AISIX_DB_HOST}" -U "${AISIX_DB_ADMIN}" -d postgres -c \
"DROP ROLE ${AISIX_DB_ROLE};"DROP DATABASE
DROP ROLEDropping the database destroys every organization, environment, model, caller API key, and budget in it. Take a backup first if there is any doubt about which database you are connected to.
Troubleshoot
The startup error names the missing capability. Read it out of the cp-api logs.
| Symptom | Cause | Fix |
|---|---|---|
cp-api database role must be SUPERUSER or have BYPASSRLS … | The connection role is missing BYPASSRLS. | As a database administrator: ALTER ROLE ${AISIX_DB_ROLE} BYPASSRLS; |
permission denied for schema public | The role cannot create objects in public. | Confirm that the role owns the database, and run step 3 if the schema is locked down. |
permission denied to create role | The connection role is missing CREATEROLE, so it cannot create cp_api_app. | As a database administrator: ALTER ROLE ${AISIX_DB_ROLE} CREATEROLE; |
permission denied to set role "cp_api_app" | The migration that grants cp_api_app membership did not finish. | Read the cp-api startup log from the beginning and fix the earlier migration error; this message is a consequence of it, not the fault. |
Related
- On-Premises Installation — the installation this database is for, including the Helm path that supports it.
- On-Premises Configuration — every
postgresql.*andexternalDatabase.*value, and the private-CA settings for the dashboard. - Upgrade AISIX — why an upgrade takes a database backup first, and why a control-plane rollback is a database restore. See Back Up the Database Before You Upgrade for the external-Postgres backup procedure.
- Network and Security — what else in a deployment holds secrets, database backups included.
- Offline Resilience — what gateways do while the control plane is restarting.