Filter:
PostgreSQL

Simple Connection Pool

Replace PgBouncer with Keel. Transaction pooling with prepared-statement virtualization for ORM-heavy applications.

mode=pool ps=virtualize PostgreSQL
PostgreSQL

Read/Write Split + Patroni HA

Automatic read/write routing with Patroni cluster discovery. Replicas get reads; primary gets writes. Sticky-primary after writes.

mode=smart Patroni read-after-write
MySQL

MySQL Group Replication

Keel as a frontend for MySQL Group Replication or Galera Cluster. Automatic role detection and failover.

MySQL Group Replication mode=smart
Cloud

AWS RDS IAM Authentication

Connect to AWS RDS or Aurora using IAM token auth. No password rotation scripts needed — Keel handles token generation and caching.

AWS IAM RDS Aurora
Security

Full TLS + mTLS Setup

End-to-end TLS with mutual client certificate verification. Includes kTLS kernel offload and TLS 1.3 enforcement.

TLS 1.3 mTLS kTLS
Kubernetes

Kubernetes + Helm Deployment

Full Kubernetes deployment with Helm, ConfigMap, Secret, HPA based on pool wait queue, and PodMonitor for Prometheus.

Helm HPA Prometheus
Operations

Docker Compose Stack

Complete Docker Compose stack: Keel + PostgreSQL + Prometheus + Grafana. Ready for local development and staging.

Docker Compose Grafana
PostgreSQL · Experimental

Horizontal Sharding

4-shard setup with hash-based routing on user_id. Includes scatter-merge aggregation and admin plan inspection.

sharding scatter-merge experimental
Hooks · Lua

Lua Query Routing Hook

Route queries based on username and time-of-day using Lua. Block heavy reports during business hours. Demonstrates all 4 hook points.

Lua 5.4 before_route mode=full
Configuration

Declarative Query Rules

Block dangerous operations, route analytics to replicas, rewrite legacy table names, and rate-limit heavy queries — all in INI config, no code.

query_rule throttle no-code

Simple PostgreSQL Connection Pool

The simplest production setup: replace PgBouncer with Keel's transaction pooling. The virtualize prepared-statement strategy transparently handles ORMs like Hibernate, GORM, and SQLAlchemy without any application changes.

keel.ini
[keel]
experimental_features = false
log_level = info

[worker_group.main]
protocol           = postgresql
bind_addr          = 0.0.0.0
bind_port          = 6432
num_workers        = 0           # auto: one per CPU core

# Production defaults
mode               = pool
prepared_statement = virtualize

# Pool sizing
min_pool_size      = 10
max_pool_size      = 50
idle_timeout       = 5m
pool_wait_timeout_ms = 5000

# Timeouts
client_connect_timeout  = 10s
client_idle_timeout     = 10m
backend_connect_timeout = 5s

# Health probes
probe          = postgres
probe_interval = 5s
probe_timeout  = 3s
probe_retries  = 3

# Admin / metrics
admin_port   = 9187
metrics      = on

[worker_group.main.servers]
primary = host=db.local port=5432 dbname=mydb user=app password=secret role=RW weight=100
Terminal
# Start
sudo systemctl start keel

# Connect through Keel
psql "host=localhost port=6432 dbname=mydb user=app"

# Check pool status
psql -h localhost -p 9187 -c "SHOW POOL"
psql -h localhost -p 9187 -c "SHOW WORKERS"

# Prometheus metrics
curl -s http://localhost:9187/metrics | grep keel_pool
  • num_workers = 0 auto-selects one worker per CPU core via sysconf(_SC_NPROCESSORS_ONLN)
  • With max_pool_size = 50 and 8 workers, max backend connections = 400. Adjust PostgreSQL's max_connections accordingly
  • prepared_statement = virtualize works with any ORM. Keel stores the parse text and re-issues it on each borrowed backend
  • Hot-reload with kill -HUP $(pgrep keel) applies pool size changes without restart

Read/Write Splitting + Patroni HA

Automatic read/write routing with Patroni cluster discovery. Patroni's REST API provides automatic role detection — no manual primary/replica assignment needed.

keel.ini
[keel]
experimental_features = false

[worker_group.prod]
protocol             = postgresql
bind_addr            = 0.0.0.0
bind_port            = 6432
num_workers          = 0

# Hardening tier: enables R/W routing
mode                 = smart
prepared_statement   = virtualize
transaction_tracking = off    # promote to 'on' only after validating failover

min_pool_size        = 10
max_pool_size        = 100

# Patroni probe — auto-detects primary and replicas
probe                = patroni
probe_host           = patroni-api.local
probe_port           = 8008
probe_interval       = 3s
probe_timeout        = 2s
probe_retries        = 3
failover_delay       = 10s

# Observability
admin_port   = 9187
metrics      = on
tracing      = off

[worker_group.prod.servers]
# role=auto: Keel detects role via pg_is_in_recovery() / Patroni
node1 = host=10.0.0.1 port=5432 dbname=prod user=app password=s role=auto weight=100
node2 = host=10.0.0.2 port=5432 dbname=prod user=app password=s role=auto weight=100
node3 = host=10.0.0.3 port=5432 dbname=prod user=app password=s role=auto weight=100
  • mode = smart is in the hardening bucket. Validate failover behavior in your environment before promoting to production
  • Sticky-primary window: after a write, reads are pinned to primary for 100 ms before replicas resume
  • Replica routing uses configurable weights — set weight=50 for replicas in geographically distant regions
  • Patroni discovery polls GET /cluster; falls back to GET /patroni. No external library dependencies — pure C HTTP/1.0 client

MySQL Group Replication

Keel handles MySQL 9, Percona XtraDB Cluster, MariaDB Galera, and Group Replication topologies with the same pooling and routing features as PostgreSQL.

keel.ini
[worker_group.mysql]
protocol        = mysql
bind_addr       = 0.0.0.0
bind_port       = 7306
num_workers     = 0
mode            = smart
min_pool_size   = 5
max_pool_size   = 100

# MySQL auth
server_auth     = caching_sha2_password

# Health probe
probe           = mysql
probe_interval  = 3s
probe_timeout   = 2s
probe_retries   = 3

admin_port      = 9188
metrics         = on

[worker_group.mysql.servers]
primary  = host=10.0.1.1 port=3306 dbname=app user=app password=s role=RW weight=100
replica1 = host=10.0.1.2 port=3306 dbname=app user=app password=s role=RO weight=100
replica2 = host=10.0.1.3 port=3306 dbname=app user=app password=s role=RO weight=100

AWS RDS IAM Authentication

Keel generates SigV4 tokens for RDS IAM authentication automatically and caches them for 14 minutes. Your application connects to Keel with a regular password and Keel handles IAM internally.

keel.ini
[worker_group.rds]
protocol           = postgresql
bind_addr          = 0.0.0.0
bind_port          = 6432
num_workers        = 0
mode               = pool
prepared_statement = virtualize
min_pool_size      = 5
max_pool_size      = 50

[worker_group.rds.servers]
# auth=aws_iam: Keel generates SigV4 tokens (cached 14 min, auto-refreshed)
primary = host=mydb.cluster-abc.us-east-1.rds.amazonaws.com \
          port=5432 dbname=mydb user=myapp \
          role=RW auth=aws_iam aws_region=us-east-1

# For Aurora with reader endpoint
reader  = host=mydb.cluster-ro-abc.us-east-1.rds.amazonaws.com \
          port=5432 dbname=mydb user=myapp \
          role=RO auth=aws_iam aws_region=us-east-1
IAM Policy (JSON)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["rds-db:connect"],
      "Resource": [
        "arn:aws:rds-db:us-east-1:123456789012:dbuser:cluster-ABC/myapp"
      ]
    }
  ]
}

Full TLS + mTLS Setup

Complete TLS configuration with mutual client authentication, kernel TLS offload, and TLS 1.3 enforcement.

keel.ini
[worker_group.secure]
protocol       = postgresql
bind_port      = 6432
mode           = pool

# Frontend TLS (clients → Keel)
tls_mode       = require
tls_cert_file  = /etc/keel/certs/server.crt
tls_key_file   = /etc/keel/certs/server.key
tls_ca_file    = /etc/keel/certs/ca.crt   # enables mTLS: verify client certs
tls_min_version = 1.3
tls_ciphers    = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256

# Kernel TLS offload (kTLS) — reduces CPU usage for TLS encryption/decryption
ktls           = on

# Backend TLS (Keel → PostgreSQL)
backend_tls    = require
backend_tls_ca = /etc/keel/certs/backend-ca.crt

# Auto-renew warning threshold
tls_auto_renew_days = 30

[worker_group.secure.servers]
primary = host=db.local port=5432 dbname=mydb user=app password=s role=RW
Shell — Generate self-signed CA + certs
mkdir -p /etc/keel/certs && cd /etc/keel/certs

# CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
  -subj "/CN=keel-ca" -out ca.crt

# Server cert
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=keel-server" -out server.csr
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt -days 365 -sha256

# Client cert (for mTLS)
openssl genrsa -out client.key 2048
openssl req -new -key client.key -subj "/CN=myapp" -out client.csr
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt -days 365 -sha256

# Verify
psql "host=localhost port=6432 dbname=mydb user=myapp \
      sslcert=client.crt sslkey=client.key sslrootcert=ca.crt sslmode=verify-full"

Kubernetes + Helm Deployment

Full Kubernetes deployment with HPA scaling and Prometheus monitoring.

values.yaml
image:
  repository: ghcr.io/virtlabs-io/keel
  tag: latest
  pullPolicy: IfNotPresent

replicaCount: 2

service:
  type: ClusterIP
  port: 6432
  adminPort: 9187

config:
  bindPort: 6432
  numWorkers: 4
  mode: pool
  preparedStatement: virtualize
  minPoolSize: 10
  maxPoolSize: 50
  adminPort: 9187
  metrics: true

# Backend credentials from Secret
envFrom:
  - secretRef:
      name: keel-db-credentials

resources:
  requests:
    cpu: 200m
    memory: 128Mi
  limits:
    cpu: 2
    memory: 512Mi

# Liveness / readiness
livenessProbe:
  httpGet:
    path: /healthz
    port: 9187
  initialDelaySeconds: 5

readinessProbe:
  httpGet:
    path: /ready
    port: 9187
hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: keel-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: keel
  minReplicas: 2
  maxReplicas: 10
  metrics:
  # Scale up when clients are waiting for pool connections
  - type: Pods
    pods:
      metric:
        name: keel_pool_wait_queue_enqueued
      target:
        type: AverageValue
        averageValue: "5"
podmonitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: keel
spec:
  selector:
    matchLabels:
      app: keel
  podMetricsEndpoints:
  - port: admin
    path: /metrics
    interval: 15s

Docker Compose Stack

Full local development stack: Keel + PostgreSQL + Prometheus + Grafana.

docker-compose.yml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 5

  keel:
    image: ghcr.io/virtlabs-io/keel:latest
    depends_on:
      postgres:
        condition: service_healthy
    ports:
      - "6432:6432"
      - "9187:9187"
    volumes:
      - ./keel.ini:/etc/keel/keel.ini:ro
    environment:
      KEEL_LOG_LEVEL: info
    restart: unless-stopped

  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
    volumes:
      - grafdata:/var/lib/grafana

volumes:
  pgdata:
  grafdata:
keel.ini
[worker_group.main]
protocol           = postgresql
bind_addr          = 0.0.0.0
bind_port          = 6432
num_workers        = 2
mode               = pool
prepared_statement = virtualize
min_pool_size      = 5
max_pool_size      = 20
admin_port         = 9187
metrics            = on

[worker_group.main.servers]
primary = host=postgres port=5432 dbname=mydb user=app password=secret role=RW

Horizontal Sharding (Experimental)

4-shard setup with hash-based routing on user_id. Scatter-merge queries like COUNT, SUM, GROUP BY work transparently.

keel.ini
[keel]
experimental_features = true

[worker_group.main]
protocol           = postgresql
bind_port          = 6432
mode               = smart
prepared_statement = virtualize

[shard_rule.orders]
table        = orders
column       = user_id
shard_count  = 4
strategy     = hash

[shard_backend.0]
host = shard0.local
port = 5432
database = mydb
user = app
password = s

[shard_backend.1]
host = shard1.local
port = 5432
database = mydb
user = app
password = s

[shard_backend.2]
host = shard2.local
port = 5432
database = mydb
user = app
password = s

[shard_backend.3]
host = shard3.local
port = 5432
database = mydb
user = app
password = s
SQL examples
-- Single-shard lookup (hash(user_id=42) → shard 2)
SELECT * FROM orders WHERE user_id = 42;

-- Scatter-merge aggregation (fans out to all 4 shards)
SELECT status, COUNT(*) AS cnt, SUM(amount) AS total
FROM orders
GROUP BY status
ORDER BY total DESC;

-- Inspect routing plan
EXPLAIN SHARD PLAN FOR 'SELECT * FROM orders WHERE user_id = $1';
--  query_type | shard_count | target_shard | strategy | key_column | estimated_rows
-- ─────────────────────────────────────────────────────────────────────────────────
--  SINGLE     |           4 |            2 | hash     | user_id    |           1042

-- List shard rules
SHOW SHARD RULES;

Lua Query Routing Hook

A Lua hook that routes analytics users to replicas, blocks heavy reports during business hours, and logs all DDL queries.

/etc/keel/hooks/route.lua
-- Hook: before_route
-- Demonstrates: custom routing, blocking, and logging

local function is_business_hours()
    local hour = tonumber(os.date("%H"))
    return hour >= 9 and hour < 18
end

function before_route(ctx)
    -- Route analytics user always to replica
    if ctx.user == "analytics" or ctx.user == "reporting" then
        ctx.route = "replica"
        return true
    end

    -- Block heavy report queries during business hours
    if is_business_hours() and ctx.sql:find("GENERATE_SERIES") then
        ctx.abort_message = "Heavy reports blocked during business hours (09:00-18:00)"
        return false
    end

    return true  -- continue with default routing
end

-- Hook: after_query_parse (mode=full required)
function after_query_parse(ctx)
    -- Log all DDL queries
    if ctx.query_type == "DDL" then
        io.write(string.format("[DDL] user=%s sql=%s\n", ctx.user, ctx.sql:sub(1, 200)))
        io.flush()
    end
    return true
end
keel.ini
[worker_group.main]
protocol     = postgresql
bind_port    = 6432
mode         = full          # hooks require mode=full

hook.before_route    = /etc/keel/hooks/route.lua
hook.after_query_parse = /etc/keel/hooks/route.lua

[worker_group.main.servers]
primary  = host=db1.local port=5432 dbname=prod user=app password=s role=RW
replica1 = host=db2.local port=5432 dbname=prod user=app password=s role=RO
replica2 = host=db3.local port=5432 dbname=prod user=app password=s role=RO

Declarative Query Rules

Powerful routing, blocking, and rewriting rules using only INI config — no Lua or Python needed.

keel.ini
[worker_group.main]
protocol = postgresql
bind_port = 6432
mode = smart

# ─── Routing Rules ───────────────────────────────────────────────────────
# Route analytics queries to replicas
[query_rule.analytics]
match    = ^\s*(SELECT|EXPLAIN).*(FROM\s+(events|metrics|audit_log))
route_to = replica

# Force admin user queries to primary
[query_rule.admin_primary]
user     = admin
route_to = primary

# ─── Blocking Rules ──────────────────────────────────────────────────────
# Block TRUNCATE in production
[query_rule.no_truncate]
match     = ^\s*TRUNCATE
block     = true
block_msg = "TRUNCATE is not allowed through this proxy"

# Block DROP TABLE for non-admin users
[query_rule.no_drop]
match     = ^\s*DROP\s+TABLE
block     = true
block_msg = "DDL not allowed — use the migration endpoint"

# ─── Rewrite Rules ───────────────────────────────────────────────────────
# Rewrite legacy table name transparently
[query_rule.rewrite_legacy_users]
match      = \blegacy_users\b
rewrite_to = users

# ─── Throttle Rules ──────────────────────────────────────────────────────
# Rate-limit expensive report queries
[throttle.heavy_reports]
match           = SELECT.*GENERATE_SERIES|SELECT.*pg_stat
rate_per_second = 2
burst           = 5

# Per-user rate limit
[throttle.api_user]
user            = api_readonly
rate_per_second = 100
burst           = 200