Enterprise Scaling Layers
Optional, composable scaling strategy for SveltyCMS: external DB connection poolers (PgBouncer/ProxySQL), Redis as distributed cache/pub-sub (optional), and reverse proxies (Nginx/Caddy/Traefik). Zero-config defaults work great for single-node; layers unlock high-concurrency, multi-instance, and managed-DB production.
On this page
SveltyCMS is designed database-agnostic and zero-dependency for core operation. A single node with SQLite + built-in L1 cache runs fast with no external services.
For enterprise scale (many concurrent users/tenants, multiple CMS instances for HA/load, managed databases with connection limits, global edge), we provide optional, well-documented scaling layers:
- External DB Connection Poolers (PgBouncer for Postgres, ProxySQL for MariaDB/MySQL, driver-level + mongos awareness for MongoDB).
- Redis (optional) — distributed L2 cache, session store, pub/sub invalidation for multi-node coherence, and edge sync.
- Smart Entropy Compression (always-on foundation with optional trained dictionary) — pre-compressed variants on cache MISS, zero-CPU serving on HITs, domain-specific dictionary for CMS payloads.
- Reverse Proxy (Nginx, Caddy, Traefik, etc.) — TLS termination, WebSocket support, trusted headers, optional rate limiting, static asset offload.
All layers are optional. The CMS detects and gracefully falls back (L1-only cache, direct DB connections, direct exposure).
This strategy draws from proven patterns at scale (Instagram’s early use of PgBouncer, OpenAI, Cloudflare, Supabase, etc.) while staying true to SveltyCMS strengths: heavy caching, LocalCMS zero-tax SDK, turbo pre-compressed paths, batch relational writes, and native multi-tenancy via tenantId.
1. External DB Connection Poolers (PgBouncer & Equivalents)
Postgres (and to a lesser extent MariaDB) has a multi-process architecture where each connection has meaningful memory and CPU cost. Managed services often enforce low max_connections (e.g. 100–500). At scale you quickly exhaust them with many app instances or bursty CMS traffic (content publishes + public reads).
Solution: Optional external pooler proxy.
Postgres + PgBouncer (Recommended for Production PG)
- Deploy PgBouncer (sidecar per instance or central) listening on 6432.
- Point SveltyCMS at the pooler via
DB_POOLER_URL(or full connection string). - Use
pool_mode = transactionfor best multiplexing. - Important: Set
prepare: false(our adapter does this automatically whenDB_POOLER_TYPE=pgbouncer+ transaction mode, or viaDB_POOLER_PREPARE=false).
Private config keys (all optional, added in 2026-06 enterprise scaling update):
DB_POOLER_TYPE=pgbouncer
DB_POOLER_URL=postgres://user:pass@pgbouncer:6432/yourdb
DB_POOLER_MODE=transaction
DB_POOLER_PREPARE=false # explicit override if needed
In code (adapters read via getDbPoolerConfig() in config-state.ts):
- Prefer
DB_POOLER_URLwhen present. - Auto-adjust
preparefor safety. - Driver-level
maxpool still applies (you pool from app → pooler; pooler pools a small number to real Postgres).
See the benchmark docs for historical context and the Instagram pattern (returning connections to pool sooner + async fan-out).
pgbouncer.ini example (minimal, production starting point):
[databases]
* = host=your-postgres port=5432
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = users.txt # or use auth_user / auth_query for production
pool_mode = transaction
default_pool_size = 25 # tune to Postgres cores / expected concurrency
max_client_conn = 1000
server_reset_query = DISCARD ALL
server_lifetime = 3600
server_idle_timeout = 600
log_connections = 0
log_disconnections = 0
Generate users.txt or use the auth_query pattern for dynamic users.
When to use:
- Managed Postgres (RDS, Azure, Cloud SQL, Neon, Supabase direct) with connection limits.
-
5–10 CMS instances or high tenant concurrency + bursts.
- You want to keep driver prepared statements for direct hot paths while scaling horizontally.
Single-node / dev / SQLite: leave unset. Zero overhead.
MariaDB / MySQL
Similar story. Use ProxySQL (or MariaDB MaxScale) as the pooler/proxy.
- Same config keys (
DB_POOLER_TYPE=proxysql,DB_POOLER_URL=mysql://...). - MariaDB adapter (mysql2) connects transparently to the pooler.
- Tune
connectionLimitin driver (we default to 100; lower when a good pooler is in front).
The driver pool + ProxySQL gives excellent results for read/write splitting, query caching at the proxy layer, and connection multiplexing.
MongoDB
MongoDB driver pooling is already excellent (maxPoolSize, minPoolSize).
- We expose these via
ConnectionPoolOptionsand pass to mongoose. - For sharded scale use mongos (the “pooler/router”).
- Set
DB_POOLER_TYPE=mongos+ appropriate URI; our adapter already supports compressors (zstd/snappy) on the wire. - Replica sets are the common HA pattern — connection string handles discovery and pooling.
No separate binary pooler needed in most cases; the driver + replica set / mongos is the scaling story.
SQLite
File-based. External “poolers” are rarely used.
Best practices (documented in adapter and this guide):
- WAL mode (enabled by default in our migrations for concurrency).
busy_timeouthandling (we have resilience).- Single writer + multiple readers is the SQLite concurrency model.
- For “scale” use multiple read replicas (litestream or manual) or simply run multiple independent SQLite files per tenant (our multi-tenant isolation supports this).
2. Redis — Optional Distributed Cache & Coordination
Redis is already fully optional.
USE_REDIS=false(or unset) → pure L1 (in-memory LRU + negative bloom + stampede protection).- Perfect for single-node, dev, edge, or SQLite-first deploys.
- When enabled (
USE_REDIS=true+ host/port/password or URL), CacheService activates L2 + pub/sub subscriber for cross-node invalidation (svelty:cache:invalidation).
What Redis unlocks:
- Shared cache across multiple CMS instances (sessions, permissions, content, API responses).
- Sub-millisecond global invalidation on publish/edit (edge sync).
- Distributed stampede protection (locks).
- Session stickiness not required (stateless instances behind LB).
Best practices for this CMS (multi-tenant, high read cache-hit ratio):
- Maxmemory policy:
allkeys-lruorallkeys-lfu(our cache is the primary consumer). - Persistence: AOF + RDB or just AOF for cache (you can lose it on restart; L1 warms on demand).
- Clustering/Sentinel: Use for HA. Our client reconnects; pub/sub works across.
- Key design: We already namespace by tenant + category. Do not share one Redis between unrelated SveltyCMS installs without prefixes.
- Memory sizing: Start with 1–4 GB for moderate sites; monitor
cache:stats/ metrics. Our L1 (500k items) + negative bloom keeps most traffic off Redis. - Security: Password + TLS in production (redis client supports via URL options).
See src/databases/cache/cache-service.ts (L1 always, L2 lazy) and redis-store.ts (tag support, multi exec).
In reconfigure() / startup we cleanly skip or cleanup if !USE_REDIS.
4. Reverse Proxy (Nginx / Caddy / Traefik) — Optional but Recommended for Prod
The CMS can run directly exposed, but a reverse proxy adds:
- TLS termination (easy certs).
- WebSocket upgrade for real-time (
/ws). - Client IP for rate limiting, audit logs, security (we already parse
X-Forwarded-*in setup-proxy tests and hooks). - Optional additional rate limiting / WAF layer (our internal firewall + rate limiter still run).
- Static asset serving or caching headers for public content.
- Load balancing multiple CMS instances.
Our internal stack already does a lot:
- Strong security headers (handle-security-headers).
- Smart pre-compression (see section 3) for turbo/API cache hits (br/gzip) + on-the-fly.
- ETag, Vary, cache-control via our layers.
- Proxy header hardening (see
index.cjsandsetup-proxy.test.ts).
Recommendations to avoid double work:
- Let Nginx handle TLS + static / long-lived assets if you want.
- For dynamic CMS responses (especially turbo pre-compressed paths and API), disable gzip/brotli in Nginx or use
gzip off;for those locations — we already ship compressed bytes with correctContent-EncodingandX-Compression-*observability headers. - Always set
proxy_set_header Host $host;,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto. - WebSocket:
proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";for/ws. - Health: proxy
/api/system/health.
TRUSTED_PROXIES (new in schema):
Set TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 (or your LB IPs) so our rate limiter, auth, and audit correctly see the real client IP instead of the proxy.
Production Nginx Example (Copy-Paste Ready)
# Nginx reverse proxy for SveltyCMS (production / enterprise)
# Place in /etc/nginx/sites-available/ and enable.
# Assumes SveltyCMS on 127.0.0.1:3000, TLS terminated here.
#
# Also set in SveltyCMS private config:
# TRUSTED_PROXIES=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
upstream sveltycms {
server 127.0.0.1:3000;
# Add more for multi-instance LB:
# server 10.0.1.10:3000;
# server 10.0.1.11:3000;
keepalive 32;
}
server {
listen 80;
server_name cms.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name cms.example.com;
ssl_certificate /etc/letsencrypt/live/cms.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/cms.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# Real client IP for rate limiter, auth, audit
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
location / {
proxy_pass http://sveltycms;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket for /ws realtime features
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
# IMPORTANT: SveltyCMS pre-compresses dynamic responses.
# Do NOT double-compress here.
gzip off;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Health check passthrough
location = /api/system/health {
proxy_pass http://sveltycms;
access_log off;
}
}
Composition Diagrams
Minimal Deployment (Single Node, SQLite)
No external dependencies — everything runs in-process:
pre-compressed HITs] E[LocalCMS SDK
zero HTTP tax] F[L1 Cache LRU
500K entries] G[SQLite WAL
in-process] end D --> F E --> G F --> G
High-Scale Enterprise Deployment (All Layers)
Every layer is optional and composable — add only what you need:
L1 Cache 500K] F1[LocalCMS] G1[Audit fire-and-forget] end subgraph D2[Instance 2] E2[Turbo pre-comp
L1 Cache 500K] F2[LocalCMS] G2[Audit fire-and-forget] end subgraph D3[Instance N] E3[Turbo pre-comp
L1 Cache 500K] F3[LocalCMS] G3[Audit fire-and-forget] end D1 -->|"pub/sub invalidation"| H[Redis Cluster] D2 -->|"session sharing"| H D3 -->|"cross-node coherence"| H D1 -->|"connection multiplexing"| I[PgBouncer :6432] D2 --> I D3 --> I I -->|"tx pooling 25 conns"| J[(PostgreSQL Primary)] J -->|"streaming replication"| K[(Read Replica 1)] J -->|"streaming replication"| L[(Read Replica 2)] D1 -.->|"read queries"| K D2 -.->|"read queries"| L D1 -.->|"write queries"| J B -.->|"purge by tag"| C
Layer Decision Flow
All layers are optional — start simple and add as you grow.
Security & Multi-Tenancy Notes
- Poolers and proxies must not bypass our 4-layer defense (middleware → dispatcher permissions → handler checks → page actions).
- Tenant isolation stays in the query layer (
tenantIdfilters or separate schemas/DBs). Poolers/proxies are transparent at the connection level. - Always validate
X-Forwarded-*only from trusted proxies (we use the newTRUSTED_PROXIES+ existing hardening). - Audit logs still capture real client IPs when proxies are trusted correctly.
When (Not) to Enable Layers
- Single node / low traffic / SQLite dev: Nothing. Best performance, zero ops.
- Multi-node or HA on managed DB with conn limits: Add PgBouncer (or equivalent) + Redis + reverse proxy.
- Edge / global: Redis pub/sub + our existing edge invalidation + CDN purge (Cloudflare config already supported).
- Monitor via built-in metrics +
database-resiliencepool diagnostics (it now recommends external poolers when pressure is high).
Related Reading
- Database Adapters Architecture
- Cache Service (L1/L2 details)
- Security Headers & Proxy Handling
- Benchmark reports (hooks, relational, cache) for before/after numbers after the hot-path + scaling work.
This gives SveltyCMS a clean, documented, optional path to the same scaling techniques used by the largest Postgres users while preserving the lightweight, database-agnostic, high-performance core that makes it special.