PostgreSQL Implementation
PostgreSQL adapter implementation using Drizzle ORM for SveltyCMS.
On this page
PostgreSQL support is production-ready. All adapter modules (Auth, CRUD, Content, Media, System, Widgets, Themes, Batch, Transactions, Performance, Cache, Collections, Multi-Tenancy) are fully implemented with Drizzle ORM.
Overview
SveltyCMS supports PostgreSQL as an alternative to MongoDB and MariaDB through the database adapter pattern. The PostgreSQL adapter uses Drizzle ORM with the postgres.js driver for optimal performance and type safety.
Architecture
The PostgreSQL adapter follows a modular pattern, leveraging Drizzle ORM for schema-agnostic operations and raw SQL for initial schema setup.
File Structure
src/databases/postgresql/
βββ postgresAdapter.ts # Entry point (re-exports adapter)
βββ migrations.ts # Automatic "CREATE TABLE IF NOT EXISTS" logic
βββ utils.ts # Error handling and data transformation
βββ adapter/
β βββ index.ts # Main adapter class with feature modules
β βββ adapterCore.ts # Core functionality (connect, disconnect, health)
βββ schema/
βββ index.ts # Drizzle ORM schema definitions
Schema Initialization (Migrations)
Unlike MongoDB which is schema-less, PostgreSQL requires table definitions. SveltyCMS implements an automatic migration system that runs during the initial setup or the first system access.
sequenceDiagram
participant S as Setup Wizard / System Init
participant A as PostgreSQLAdapter
participant M as Migrations
participant D as PostgreSQL DB
S->>A: ensureSystem()
A->>M: runMigrations(sql)
M->>D: CREATE TABLE IF NOT EXISTS ...
D-->>M: Tables Created/Verified
M-->>A: { success: true }
A-->>S: Ready
Connection Pool Configuration (2026-07 Optimization)
SveltyCMS configures the postgres.js driver with production-tuned connection pool settings. These can be overridden via environment variables or connection config object:
| Setting | Default | Env Var Override | Purpose |
|---|---|---|---|
max |
200 | DATABASE_MAX_CONNECTIONS |
Maximum connections in the pool |
connect_timeout |
10s | β | 3Γ faster failure detection on unreachable hosts |
idle_timeout |
300s | β | Pooled connections stay warm 5Γ longer; reduces SSL handshake overhead |
max_lifetime |
60 min | β | Halves pool recreation overhead vs. previous 30 min default |
keepalive |
true | β | Prevents silent connection drops from cloud NAT/proxies/firewalls |
keepaliveInitialDelayMillis |
10000 | β | Starts keepalive pings after 10s idle |
pipeline |
true | β | Postgres.js query pipelining; batches concurrent queries into single TCP round-trips (up to 3Γ throughput on parallel workloads) |
prepare |
true | DATABASE_PREPARE=false |
Server-side prepared statements; disable for PgBouncer transaction mode |
statement_timeout |
30s | β | Safety limit for individual queries |
application_name |
sveltycms |
β | Identifies the CMS in pg_stat_activity |
Connection Configuration
PostgreSQL connections are configured in config/private.ts or during the setup wizard:
// Configuration Object
{
type: 'postgresql',
host: 'localhost',
port: '5432',
name: 'sveltycms',
user: 'postgres',
password: 'your_password'
}
Drizzle Schema
The PostgreSQL schema mirrors the CMS data model but is optimized for relational performance.
Key Implementation Details
| Feature | Implementation |
|---|---|
| Primary Key | varchar(36) (UUID compatible strings) |
| Timestamps | timestamp() with ISODateString conversion |
| JSON fields | jsonb() for binary JSON with GIN indexing |
| Multi-Tenancy | tenantId indexed columns on all tables |
| Connection Pool | Keepalive (10s), pipeline (batch), idle 300s, lifetime 60min, timeout 10s |
Current Implementation Status
β All Modules Implemented
| Module | Status | Notes |
|---|---|---|
auth.* |
β Complete | Users, sessions, tokens, roles with compound indexes |
crud.* |
β Complete | Full CRUD with query builder and batch operations |
content.* |
β Complete | Nodes, drafts, revisions with JSONB data storage |
media.* |
β Complete | File metadata, folders, thumbnails |
batch.* |
β Complete | Transactional batch operations |
performance.* |
β Complete | Latency tracking and health monitoring |
| tenants.* | β
Complete | Full CRUD (create, getById, update, delete, list) |
| cleanupExpired | β
Complete | TTL-equivalent cleanup for sessions/tokens |
| versioning | β
Complete | Atomic getVersion and incrementVersion |
PostgreSQL-Specific Optimizations
-
JSONB (not JSON): All 20 metadata columns use binary JSONB with efficient containment operators (
@>,?,?|) -
GIN Indexes: 5 GIN indexes on high-query JSONB columns (
content_nodes.data,content_nodes.metadata,media_items.metadata,roles.permissions,auth_users.roleIds) -
Trigram Search: High-performance
ilikesearching for media filenames usingpg_trgmGIN indexes (media_items_filename_trgm_idx). -
gen_random_uuid(): Native UUID generation via pgcrypto extension -
Tenants Table: Full multi-tenancy with quota, usage, and settings JSONB columns
-
Website Tokens:
website_tokensvia sharedrelational-system.tsβ SHA-256 hash at rest, unique index ontoken, compound{ tenantId, name }, parallel list+count. See Credential Storage.
Development Guide
To extend the PostgreSQL adapter:
1. Schema Updates
Modify src/databases/postgresql/schema/index.ts. Remember to also update src/databases/postgresql/migrations.ts to ensure tables are created correctly for new users.
2. Implementing Methods
Implement methods in src/databases/postgresql/adapter/index.ts. Use the wrap() helper for consistent error handling and logging.
public readonly crud = {
findOne: async (collection: string, query: Record<string, unknown>) => {
return this.wrap(async () => {
const table = this.getTable(collection);
const where = this.mapQuery(table, query);
const result = await this.db!.select().from(table).where(where).limit(1);
return result[0] || null;
}, 'CRUD_FIND_ONE_FAILED');
}
};
Performance Benchmarks
The PostgreSQL adapter is optimized for relational efficiency and JSONB search performance. For detailed performance metrics and comparisons, please refer to the Performance Benchmarks document.
Key PostgreSQL highlights:
- JSONB with GIN Indexing: 0.063 ms NATIVE UPSERT, 0.083 ms FIND MANY, 1.201 ms FIND ONE.
- Partial Indexes: Optimized performance for active sessions and tokens.
- Connection Health: Robust pooling via
postgres.jswith integrated health checks.
Key Optimizations
- JSONB with GIN Indexing: All 20 metadata columns use PostgreSQLβs binary
jsonbtype with 5 GIN indexes for sub-10ms search on complex containment queries. - Partial Indexes: Only index active sessions and unconsumed tokens β reduces index size and speeds up common auth queries.
- Connection Health: Optimized pooling via
postgres.jswith health checks, reconnection, keepalive, and query pipelining for up to 3Γ throughput on parallel workloads. - TTL-Equivalent Cleanup:
cleanupExpiredData()method purges expired sessions and consumed tokens (equivalent to MongoDBβs TTL indexes). - Tamper-Evident Logs: Relational structure with transactional integrity for cryptographic audit log chaining.
π Production Scaling with PgBouncer (Enterprise)
For high-concurrency Postgres deployments or managed databases with connection limits (RDS, Azure, Cloud SQL), SveltyCMS recommends deploying PgBouncer as a connection proxy layer. This follows the same pattern used at scale by Instagram, OpenAI, and other Postgres-heavy platforms.
Why PgBouncer
- Connection Multiplexing: PgBouncer sits between your app instances and Postgres, pooling hundreds of lightweight client connections onto a small pool of real Postgres backend connections. This prevents βtoo many clientsβ errors on managed DBs (often limited to 50-200 connections).
- Lower Latency: App connections to PgBouncer are cheap (Unix socket or localhost). Real Postgres connection setup (~50ms) is avoided on every request β PgBouncer reuses existing backend connections.
- Reduced Postgres CPU: Fewer Postgres backend processes mean fewer context switches and less memory pressure in Postgresβs multi-process model.
- Horizontal Scaling: Adding more app instances doesnβt require increasing Postgres
max_connectionsβ they all share PgBouncerβs pool.
Deployment Pattern
Deploy PgBouncer as a sidecar on each app instance (localhost:6432) or as a central proxy behind a lightweight load balancer. The sidecar pattern minimizes network latency.
Recommended Configuration
Create /etc/pgbouncer/pgbouncer.ini:
[databases]
sveltycms = host=127.0.0.1 port=5432 dbname=sveltycms
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
;; Transaction pooling β best efficiency for web workloads.
;; Connections are returned to the pool immediately after each transaction.
pool_mode = transaction
;; Conservative defaults. Tune based on CPU cores and workload.
default_pool_size = 25
max_client_conn = 500
;; Cleanup between client uses (prevents session state leakage)
server_reset_query = DISCARD ALL
;; Logging for observability
log_connections = 1
log_disconnections = 1
stats_period = 60
SveltyCMS Integration
When using PgBouncer in transaction mode, set the following in your environment or config/private.ts:
# Point the app to PgBouncer instead of direct Postgres
DB_HOST=127.0.0.1
DB_PORT=6432
# Required: Disable server-side prepared statements when behind PgBouncer
# (PgBouncer tx mode cannot guarantee the same backend connection between
# prepare and execute, causing "prepared statement does not exist" errors)
DATABASE_PREPARE=false
The adapter automatically detects DATABASE_PREPARE=false and sets prepare: false in the postgres.js driver. This trades the ~40% parse/plan win from server-side prepared statements for seamless PgBouncer compatibility. The query planner cache in Postgres still provides significant optimization.
Session State Limitations: Transaction pooling does NOT preserve session-level state (SET statements, temporary tables, LISTEN/NOTIFY, certain cursor types, advisory locks). SveltyCMS does not depend on any of these β all state is managed at the application layer via our cache, auth sessions, and LocalCMS SDK.
Monitoring
Check PgBouncer health via its admin console:
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
> SHOW POOLS;
> SHOW STATS;
> SHOW CLIENTS;
SveltyCMSβs built-in ConnectionPoolOptions and getConnectionPoolStats() work transparently whether connecting directly or through PgBouncer. For production, monitor:
cl_waiting(should stay near 0 β increasedefault_pool_sizeif growing)avg_wait_time(should be < 1ms)- Postgres
activeconnections (should be stable, not climbing)