PostgreSQL or MongoDB — pick one. Prisme.ai’s three structured-data stores (
users, permissions, collections) run on a single relational engine. The choice is per-deployment — you cannot mix Postgres and Mongo across these three.We recommend PostgreSQL for new deployments: cheaper managed offerings, ubiquitous ops know-how, Entra ID / IAM passwordless auth on Azure, and simpler backups. See MongoDB only if your team already runs Mongo at scale.Role in the platform
PostgreSQL holds three logical databases:users— user accounts and authentication metadata, plus all organization-level data: organizations themselves, memberships, groups, custom roles, API keys and service accounts. Used byprismeai-api-gateway.permissions— workspace-level access-control rules, roles and memberships (workspaces, pages, apps…). Used byprismeai-workspaces,prismeai-eventsandprismeai-runtime. Note: org-level RBAC lives inusers, not here.collections— workspace data, automation state and product collections. Used byprismeai-runtime.
Version compatibility
- Minimum: PostgreSQL 12+ (14+ recommended).
sslmode=requireis mandatory on all connection strings if ssl is enforced by postgresql server.
Recommended deployment
Topology: primary + at least one standby replica, multi-AZ.
Minimum hardware: 2 vCPU, 2 GB RAM Minimum disk: 10 GB per node, to grow with usage. By default, the three databases (
users, permissions, collections) live on a single shared cluster — keeps the initial deployment simple.
Under sustained high load, the auth path (
users + permissions, critical on every request) can be impacted by collections growth — these have very different access patterns. If a single cluster proves insufficient, split into two PostgreSQL clusters: one for users + permissions, one for collections. They can then be sized and backed up independently.Configuration
Three Helm keys change compared to MongoDB. Each is marked with apostgresql comment in the chart values.yaml.
URL example: postgres://user:password@db.example.com:5432/permissions?sslmode=require.
prismeai-core-values.yml
Backup & restore
Backup with pg_dump
Restore with pg_restore
Managed services
- RDS: automated daily snapshots + point-in-time recovery.
- Azure Flexible Server: built-in backup with configurable retention.
- Cloud SQL: automated backups with point-in-time recovery.
Updates
- Schema migrations run automatically on backend startup.
- For major Postgres version jumps (e.g. 12 → 14), follow the cloud provider’s blue/green or in-place upgrade procedure. Snapshot first.
Scaling
- Vertical first: PostgreSQL scales well on a single beefy primary up to most workloads.
- Read replicas when read load is heavy and your application can tolerate stale reads.
- Connection pooling: deploy PgBouncer in front of Postgres for high-concurrency workloads.
- Indexes: monitor with
pg_stat_statements. Common hot paths are user lookups and workspace-scoped queries.
Azure Entra ID passwordless auth
On Azure DB for PostgreSQL Flexible Server, use Entra ID Workload Identity to inject short-lived tokens instead of static passwords. ConfigureazureManagedIdentityClientId on the relevant Helm storage blocks — see Azure deployment notes.
Troubleshooting
remaining connection slots are reserved for roles with the SUPERUSER attribute
The PostgreSQL cluster has reached its max_connections limit — non-superuser roles can no longer open new connections because the remaining slots are reserved for superusers (see superuser_reserved_connections).
How many connections does each pod open?
Every backend pod talking to PostgreSQL keeps one connection pool per database it consumes. Each pool grows on demand to serve concurrent queries, so the count varies between an idle baseline and a saturation cap. The figures below assume the default pool cap (max=10 sockets per pool):
Notes:
prismeai-api-gatewaydoes not connect to thepermissionsDB. Its org-level RBAC (organizations, memberships, service accounts) is stored in theusersDB alongside the user accounts. ThepermissionsDB only holds workspace-level RBAC, consumed byworkspaces,eventsandruntime.- The runtime is multi-threaded: each worker thread opens its own pools in addition to the main thread, so a runtime pod with
RUNNER_MAX_THREADS=4(the recommended setting for large loads) holds 5 pools topermissionsand 8 pools tocollections— by itself it can consume 50 + 80 = 130 sockets at peak. - HPA scale-out multiplies these numbers linearly with the replica count of every service.
RUNNER_MAX_THREADS=4 and 2 pods of each backend service:
In practice, the steady-state usage stays very close to the Min column — pools rarely all hit
max=10 simultaneously, because that requires every pool to face a concurrent burst at the same instant. The Max column is a theoretical worst case, not a sizing target.
For this deployment, sizing max_connections ~= 100 (Min + a small headroom) is enough under normal traffic. You only need to approach the 340 ceiling if you regularly see large concurrent bursts hitting every service at once, or simply if you need to scale the runtime to many pods — each additional runtime replica adds 5 permissions pools + 8 collections pools, so the high-water mark grows fast.At that point PgBouncer is a far better answer than raising
max_connections, because it removes the linear coupling between pod count and Postgres backend count entirely.
Inspecting current usage
Check how connections are distributed across databases — this tells you whether one DB (typicallycollections) is responsible for the saturation:
Resolving the error
- Cap the per-pod pool size via env vars —
PERMISSIONS_STORAGE_POOL_MAX=5,COLLECTIONS_STORAGE_POOL_MAX=3,USERS_STORAGE_POOL_MAX=5. This is the lowest-friction fix and bounds each pod’s budget without restarting Postgres. See connection pool internals — tuning levers for the full list. - Raise
max_connectionson the cluster (requires a restart on most managed services). Add headroom for migrations and admin sessions on top of the steady-state usage. - Reduce replicas when possible — lower HPA
maxReplicas, orRUNNER_MAX_THREADSon the runtime (each unit removed = -1permissionspool and -2collectionspools per runtime pod). - Put a connection pooler in front of Postgres (see below). This is the durable fix for any deployment that scales horizontally.
Connection pooling with PgBouncer
For any deployment that scales horizontally, a connection pooler in front of Postgres is not optional — it is the structural fix that lets Prisme.ai scale at all without crushing the database.Why Postgres can’t absorb the connections on its own
PostgreSQL uses a one-process-per-connection model: every open client connection spawns a dedicated OS backend process on the server, holding ~5–10 MB of RSS plus its own working mem allocations, file descriptors, and scheduler footprint. This design is excellent for query isolation but scales terribly with connection count:- Memory blows up linearly — 500 connections = 2.5–5 GB of backend RAM just to hold the processes open, before any query runs. On a small managed instance (4–8 GB total), that swallows most of the shared_buffers budget.
- CPU context-switching degrades as the kernel juggles hundreds of mostly-idle processes.
- Even idle connections cost — a connection sitting in
idlestate still holds its backend process; it doesn’t “go to sleep” for free.
max_connections aggressively (typically ~100 on small tiers, ~300–600 on medium ones): raising the cap doesn’t free you, it just shifts the wall further out before the same RAM limit hits.
Now combine that with how Prisme.ai pods open connections ((1 + RUNNER_MAX_THREADS) × N_runtime permissions pools, 2 × RUNNER_MAX_THREADS × N_runtime collections pools, all multiplied by HPA replicas): the count grows on every scaling axis at once.You hit
max_connections long before you hit the CPU/RAM ceiling of the database itself — i.e. the cluster could happily serve the query load, but it runs out of connection slots first.
What PgBouncer changes
PgBouncer is a small, single-binary proxy that decouples client sockets from backend processes. The Prisme.ai pods open thousands of cheap client connections against PgBouncer, while PgBouncer maintains only a small fixed pool of expensive real connections against Postgres. In transaction mode — the recommended mode for Prisme.ai — PgBouncer keeps a fixed server pool of backend connections permanently open and warm against Postgres. When a pod begins a transaction, PgBouncer hands it one of these warm connections; onCOMMIT/ROLLBACK, the connection is immediately returned to the pool and made available to the next caller. Between transactions the server connection sits idle but stays open — it never has to be re-established.
Practical consequence: the size of your Postgres backend pool is decoupled from the number of pods you run. You can scale Prisme.ai horizontally (more api-gateway/workspaces/runtime replicas, more runtime worker threads) without changing the load on Postgres — only the number of concurrently executing queries matters, and that is bounded by default_pool_size, not by pods × pool_size.
The MikroORM / pg pools inside each Prisme.ai pod work transparently against transaction-mode PgBouncer; the platform does not use session-scoped features (LISTEN/NOTIFY, advisory locks, SET outside a transaction) that would break it.
What happens when the pool is full
Ifdefault_pool_size=10 and an 11th query arrives concurrently, PgBouncer does not return an error — it queues the request until one of the 10 server connections is released. For Prisme.ai workloads (short auth lookups, RBAC checks, small writes) transactions typically last a few milliseconds, so the queue drains almost instantly even with a small server pool. A client only sees an error if its wait exceeds query_wait_timeout (default 120s).
So default_pool_size should be read as “max queries simultaneously executing on Postgres” — not “max queries the platform can serve”. The hundreds of client sockets opened by the Prisme.ai pods are absorbed by PgBouncer (max_client_conn, typically set to a few thousand) and never reach Postgres.
Where to deploy it
When self-deploying, the Prisme.ai-side cluster is the most common location: lowest RTT from the client pools, decoupled from the DB lifecycle. Expose PgBouncer as a
Service and point the Helm storage URLs at it instead of at the Postgres host.
When to deploy it
The pattern to watch for is recurring
remaining connection slots are reserved... errors during traffic peaks, even though the Postgres CPU and memory usage stay moderate. That is the signature of connection-slot exhaustion rather than database overload — exactly what PgBouncer is built to absorb.