External Database
By default the on-premises control plane bundles its own PostgreSQL. To use a database your own DBA team manages instead, you disable the bundled instance (postgresql.builtin=false) and point the control plane at an external database through the externalDatabase.* values — see On-Premises Configuration.
This page covers what that external database must provide: the role and privileges the control plane needs, and a setup that does not require a PostgreSQL superuser.
The control plane has two services that share 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 and runs no migration.
Requirements at a Glance
- PostgreSQL 14 or later. 16 is the tested and bundled version.
- One empty database, owned by the role the control plane connects as.
- One login role with
CREATEROLEandBYPASSRLS— but notSUPERUSER. - No PostgreSQL extensions. UUIDs use the built-in
gen_random_uuid().
Why the Control Plane Needs These Privileges
On its first start, cp-api builds its own schema and a dedicated low-privilege role. That bootstrap is why the connection role needs a specific, bounded set of privileges — and why it does not need a superuser:
- Ownership of the database lets cp-api create the
publicandauthschemas and every table, index, function, and Row-Level Security (RLS) policy. A database owner implicitly hasCREATEon thepublicschema under default privileges, so no separate schema grant is required on a standard cluster. CREATEROLElets cp-api create and configure its internal application role,cp_api_app, on first boot. Every per-request tenant query runs as this role, and RLS policies keyed on it isolate each organization's rows. You do not create this role yourself.BYPASSRLSis needed because a few control-plane operations are intentionally cross-tenant — caller-token authentication, billing webhooks, the background budget aggregator, and everything dp-manager does — and run on a shared connection that must see every organization's rows. Per-request tenant traffic still drops tocp_api_app, which has neitherSUPERUSERnorBYPASSRLS, so tenant isolation is preserved where it matters.
None of these is a superuser privilege.
Create the Database and Role
Run this once, as a database administrator. On a self-managed server this is the postgres superuser. On a managed service (Amazon RDS, Google Cloud SQL, Alibaba Cloud RDS), it is the instance's master/admin user — which can grant CREATEROLE and BYPASSRLS without being a true superuser.
-- 1) A dedicated login role for the control plane. Not a superuser.
CREATE ROLE aisix LOGIN PASSWORD 'change-me-to-a-strong-password'
NOSUPERUSER CREATEROLE BYPASSRLS;
-- 2) A dedicated database owned by that role.
CREATE DATABASE aisix_cloud OWNER aisix;
Choose your own role name, password, and database name. Ownership is what lets aisix create objects in public and add the auth schema on first boot.
The password is embedded in a postgres:// connection URL, so characters such as +, /, and = (common in openssl rand -base64 output) can break the DSN unless they are percent-encoded. Generate a URL-safe value instead, for example with openssl rand -hex 24. See On-Premises Deployment.
public schemaThe setup above assumes the database's public schema keeps its default privileges, where the owner can create objects. If your DBA has hardened public (for example revoked the default CREATE), also grant it explicitly to the role, connected to the new database:
\c aisix_cloud
GRANT USAGE, CREATE ON SCHEMA public TO aisix;
cp_api_app roleThe control plane creates and configures cp_api_app during migration and asserts its attributes. Pre-creating it — especially with different attributes — can make the migration fail its safety checks.
Point the Control Plane at the Database
With Helm, set the externalDatabase.* values (and postgresql.builtin=false) as described in On-Premises Configuration. The chart builds the connection URLs for both services from those values.
Under the hood — and for a Docker Compose deployment — the two services read a standard PostgreSQL connection URL from the environment. Use the same role and database for both:
AISIX_CLOUD_DATABASE_URL="postgres://aisix:<url-encoded-password>@db.internal.example.com:5432/aisix_cloud?sslmode=require"
AISIX_DPMGR_DATABASE_URL="postgres://aisix:<url-encoded-password>@db.internal.example.com:5432/aisix_cloud?sslmode=require"
AISIX_CLOUD_DATABASE_URL— read by cp-api, which runs the migration on startup.AISIX_DPMGR_DATABASE_URL— read by dp-manager. If the schema is not ready yet (cp-api is still migrating on a fresh cluster), dp-manager retries for up to ~2 minutes.
Set sslmode=require (or stricter, such as verify-full) whenever the database is reached over a network you do not fully control.
What the Control Plane Creates
On the first successful start, cp-api builds the full schema in the database you provisioned, and re-runs the same steps idempotently on every later start:
- the
publicschema — all control-plane tables (organizations, environments, models, caller API keys, budgets, and so on), owned by your role; - the
authschema — authentication tables (users, sessions); - the
cp_api_approle —NOSUPERUSER NOBYPASSRLS NOLOGIN, granted only CRUD on control-plane tables and read/write on a few non-secret identity columns; it is the role every tenant request drops to; - Row-Level Security policies that scope each tenant table to a single organization.
The connection role is automatically made a member of cp_api_app so it can switch to that role per request — you do not grant this yourself.
Privileges the Connection Role Holds
For review by your DBA, the full set the control plane relies on:
| Capability | Why it is needed |
|---|---|
LOGIN | the services connect as this role |
| owns the database | create and alter the schemas, tables, indexes, functions, and RLS policies |
CREATEROLE | create and manage the internal cp_api_app role |
BYPASSRLS | cross-tenant control-plane paths and dp-manager read every organization's rows on the shared connection |
not SUPERUSER | the control plane never needs it |
Verify the Setup
After the control plane starts, connect as an administrator and confirm the bootstrap looks right.
The internal role exists and is correctly de-privileged:
SELECT rolname, rolsuper, rolbypassrls, rolcanlogin
FROM pg_roles WHERE rolname = 'cp_api_app';
-- expect: cp_api_app | f | f | f
Both schemas were created:
SELECT nspname FROM pg_namespace WHERE nspname IN ('public', 'auth');
-- expect: two rows
Troubleshooting
permission denied to alter role ... Only roles with the SUPERUSER attribute may change the SUPERUSER attribute
The control-plane image predates non-superuser support. Upgrade to a version that supports a non-superuser role, or, as a stop-gap, connect as a superuser.
cp-api database role must be SUPERUSER or have BYPASSRLS ...
The connection role is missing BYPASSRLS. Grant it as a privileged admin:
ALTER ROLE aisix BYPASSRLS;
permission denied for schema public (on CREATE TABLE during migration)
The role cannot create objects in public. Confirm it owns the database, and on a hardened cluster grant the schema privilege explicitly:
\c aisix_cloud
GRANT USAGE, CREATE ON SCHEMA public TO aisix;
permission denied to create role
The connection role lacks CREATEROLE:
ALTER ROLE aisix CREATEROLE;
permission denied to set role "cp_api_app"
The migration grants this membership automatically, so this points to a migration that did not finish — check the cp-api startup logs for an earlier error and resolve that first.
Next Steps
- On-Premises Deployment — install the control plane with Docker Compose or Helm.
- On-Premises Configuration — the full set of Docker Compose environment variables and Helm values.