The database
we default to.
jsonb, arrays, extensions, window functions, and a query planner that gets out of the way. Postgres is the right answer for most production workloads — this page is how we use it.
PG 16
Default for new projects
30 yrs
Production-tested maturity
99.99%
Uptime across managed clusters
5–10 d
DBA/engineer placement
The stack
Tools we ship with.
PostgreSQL 16
Database
pgBouncer
Connection pool
pgBackRest
Backups
Patroni
HA failover
pgAdmin
Admin UI
Prisma
ORM (TS)
Drizzle
ORM (TS)
SQLAlchemy
ORM (Python)
Hasura
GraphQL layer
Supabase
Managed PG
Neon
Serverless PG
TimescaleDB
Time-series
Why PostgreSQL
Postgres is the rare database that's both mature and modern. It's been in production for 30 years, yet it ships features faster than any competitor — jsonb, arrays, full-text search, window functions, CTEs, extensions, and now pgvector.
Postgres vs others
Every database is a set of trade-offs. Here's when we pick Postgres, and when we don't.
PostgreSQL
DefaultType
Relational + JSONB
Best when
Complex queries, correctness, extensions
Signals
- ACID correctness matters
- Complex joins, windows, CTEs needed
- JSONB can replace a document DB
- Extensions useful (pgvector, PostGIS)
- You want one DB, not three
MySQL
Type
Relational
Best when
Simple schema, extreme read throughput
Signals
- Read-heavy with a simple schema
- Existing MySQL ecosystem
- Team already knows MySQL deeply
- Replication topology is straightforward
- You need broad hosting options
MongoDB
Type
Document
Best when
Schema-free, flexible payloads
Signals
- Schema changes constantly
- Documents are naturally nested
- Horizontal sharding is required
- You don't need joins
- Event-sourcing with flexible shapes
SQLite
Type
Embedded
Best when
Local-first, single-node, edge
Signals
- Single-node app (desktop, mobile)
- Edge or offline-first workloads
- Testing fixtures and mocks
- Read-heavy with no concurrency
- You want zero ops overhead
Data types we reach for
One of the reasons we pick Postgres over MySQL is the type system. Choosing the right type eliminates a whole class of application bugs.
| Type | Use | Note |
|---|---|---|
| uuid | Primary keys, external IDs | Generated with gen_random_uuid() |
| jsonb | Flexible payloads, event data | Indexable, queryable — unlike json |
| text[] | Tags, permissions, small lists | Native array, indexed with GIN |
| timestamptz | Every timestamp, always | Never use timestamp — timezone bugs |
| numeric | Money, precise decimals | Not float — no rounding surprises |
| inet / cidr | IP addresses, CIDR ranges | Native validation, indexing |
| tsvector | Full-text search | Built-in, no separate search engine |
| range | Date ranges, booking windows | Overlap constraints built in |
| interval | Durations, retention windows | First-class time arithmetic |
| vector | Embeddings (pgvector) | Similarity search on AI features |
Extensions we use
Postgres ships with a plugin system. These are the extensions that appear on almost every project we run.
pg_stat_statements
Query performance tracking. First thing we install on any production DB.
pgcrypto
Encryption, hashing, and UUID generation. Used for sensitive columns.
pg_trgm
Trigram similarity. Fast ILIKE queries, fuzzy search, autocomplete.
postgis
Geospatial. Points, distances, geofences — used in logistics and field apps.
pg_partman
Table partitioning automation. For time-series and event tables at scale.
pgvector
Vector similarity. Embeddings, RAG, semantic search — AI features.
pg_cron
Scheduled jobs inside the DB. Vacuum, materialized view refresh, custom tasks.
pg_repack
Online table and index rebuild without locks. Eliminates bloat without downtime.
SQL patterns we use
Real snippets from production systems — schema design, complex queries, full-text search, pgvector, and zero-downtime migrations.
-- Multi-tenant invoices table
CREATE TABLE invoices (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id uuid NOT NULL REFERENCES tenants(id),
customer_id uuid NOT NULL REFERENCES customers(id),
amount numeric(14,2) NOT NULL CHECK (amount > 0),
currency text NOT NULL DEFAULT 'GHS',
status text NOT NULL DEFAULT 'pending',
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
tags text[] NOT NULL DEFAULT '{}',
created_at timestamptz NOT NULL DEFAULT now()
);
-- Indexes that matter
CREATE INDEX idx_invoices_tenant ON invoices (tenant_id, created_at DESC);
CREATE INDEX idx_invoices_customer ON invoices (customer_id);
CREATE INDEX idx_invoices_pending ON invoices (tenant_id) WHERE status = 'pending';
CREATE INDEX idx_invoices_metadata ON invoices USING GIN (metadata jsonb_path_ops);
CREATE INDEX idx_invoices_tags ON invoices USING GIN (tags);Indexing strategy
Most slow queries are missing indexes — or have the wrong one. These are the rules we apply.
Index foreign keys
Postgres doesn't do it automatically. Missing FK indexes are the #1 cause of slow joins.
Partial indexes for status columns
If 95% of rows have status='active', index WHERE status != 'active'. Smaller, faster.
Composite indexes — order matters
Index (user_id, created_at) is used for user_id alone, but not created_at alone.
GIN for jsonb and arrays
Regular B-tree can't index jsonb. GIN can, and it's fast for containment queries.
Covering indexes with INCLUDE
Add columns to the leaf level so the query is answered from the index alone.
Don't over-index
Every index slows writes. We add indexes in response to slow queries, not in anticipation.
Performance tuning
Postgres is fast by default. When it isn't, we measure before we change anything.
EXPLAIN ANALYZE first
We never guess what's slow. EXPLAIN ANALYZE with real data, then optimise the actual bottleneck.
Connection pooling
PgBouncer in transaction mode. Postgres has finite connections — the pool is a boundary, not an afterthought.
Partition large tables
Anything over ~50M rows gets partitioned by time. Keeps indexes small and queries fast.
VACUUM and autovacuum tuning
Default autovacuum settings are wrong for high-write tables. We tune per table.
Observability
You can't tune what you can't see. Every Postgres instance we manage has these wired up from day one.
pg_stat_statements
Slow queries, call counts, mean time. The first extension we install.
pg_stat_activity
Active sessions, blocked queries, waits. Real-time view of what's happening.
Lock monitoring
Deadlocks, lock waits, lock ordering. Detects contention before it cascades.
Bloat monitoring
Table and index bloat, autovacuum effectiveness. Runs continuously.
Connection saturation
Pool usage, idle vs active. Alerts before the pool is exhausted.
Replication lag
Seconds behind, WAL position drift. Alerts when replicas fall behind.
Partitioning
Once a table crosses tens of millions of rows, partitioning keeps indexes small and queries fast. It's a design decision, not a rescue mission.
Range partitioning
By time — the most common. Days, weeks, or months per partition.
List partitioning
By region, tenant, or category. Keeps hot partitions small.
Hash partitioning
Even distribution at scale. Useful when there's no natural key.
pg_partman
Automated partition creation and cleanup. Runs on a schedule via pg_cron.
When to partition
50M+ rows, or when one partition would dominate the table.
When not to
Small tables, or queries that cross many partitions frequently.
pgvector · AI features in your database
AI without a second database.
pgvector lets you store embeddings alongside your relational data. No separate vector store, no sync pipeline, no lost transactions between two systems. Cosine similarity, inner product, L2 — all as SQL.
Semantic search
Find documents by meaning, not keywords. Embed query, cosine similarity, ranked results.
RAG pipelines
Retrieval-augmented generation. Store embeddings next to source text — one query, both.
Recommendations
Similar items, related products, 'users who liked this also liked'. Vector similarity, native.
Image similarity
Visual search, deduplication, reverse image lookup — using CLIP or similar embeddings.
Replication & HA
High availability on Postgres is well-trodden ground. We use the standard tools and drill the failover regularly — a failover that hasn't been tested doesn't count.
Streaming replication
Physical, byte-for-byte. Primary writes to a WAL stream; standbys replay it.
Logical replication
Row-level, transformable. Partial replication, cross-version, cross-schema.
Automatic failover
Patroni, RDS Multi-AZ, or managed equivalents. Tested failover drills are standard.
Read replicas
Routing strategy in the application layer. Lag-awareness for strongly consistent reads.
RTO and RPO
Documented for every client. Not aspirational numbers — measured in live drills.
Failover drills
Every quarter, on purpose. If the failover hasn't been tested, it doesn't work.
Migrations
Schema changes are where production databases get hurt. Zero-downtime is a discipline, not a feature.
Version-controlled files
Every migration is a file in the repo. Applied by CI, never by hand.
Rollbacks included
Every migration has an up and a down. Rollback tested before merge.
Zero-downtime techniques
ADD COLUMN with defaults, concurrent index creation, NOT VALID constraints, batched backfills.
What we never do
LOCK TABLE, ALTER COLUMN TYPE on live tables, DROP COLUMN without deprecation.
Squash strategy
Long-running schema evolution is squashed into clean migrations after merge.
Data vs schema
Schema changes are migrations. Data migrations are separate, idempotent, and re-runnable.
ORMs
We use ORMs — but not exclusively. Typed queries for CRUD, raw SQL for the queries that matter.
Prisma
TypeScriptType-safe, migrations built-in, great DX. Our default for TS projects.
Drizzle
TypeScriptSQL-like syntax, minimal abstraction. For teams that want to see the SQL.
SQLAlchemy
PythonThe Python standard. 2.0 syntax is excellent — typed and composable.
Eloquent
PHPLaravel's ORM. Great for CRUD, less so for complex analytics.
Raw SQL
AnyFor complex reports, aggregations, and anything performance-critical.
Mix and match
AnyORMs for CRUD, raw SQL for the 5% of queries that need it. That's the pattern.
Security
Every production database we manage runs with the same baseline: encrypted at rest (KMS or filesystem), TLS for all connections, no superuser access for application roles, and audit logging on anything sensitive.
Application code connects with a role that only has the permissions it needs — SELECT/INSERT/UPDATE on specific tables, never DDL. Migrations run under a separate role, applied by CI, never by hand.
Sensitive columns get column-level encryption via pgcrypto. PII is minimised — if the app doesn't need it, the database doesn't store it.
Backups & recovery
Continuous WAL archiving to object storage, plus daily base backups. Point-in-time recovery tested quarterly — every client has a documented RTO and RPO, and we've actually run the restore to prove it works.
Backups are stored in a separate region from the primary database. Cross-region restoration is a drill, not a hope.
Anti-patterns
The things that cause performance problems in production — and that we never do.
SELECT * in production code
Fetch only the columns you need. Wide rows cost bandwidth, memory, and cache.
ORDER BY random()
Full table scan every time. Use sampling tables or TABLESAMPLE.
Sequences as distributed PKs
Global sequences become a bottleneck. Use UUIDs or ULIDs at scale.
Missing FK indexes
Postgres doesn't create them. Missing FK indexes cause cascading slow joins.
timestamp without timezone
Timezone bugs in every report. Always use timestamptz.
Long-running transactions
Blocks vacuum, bloats tables, holds locks. Keep transactions short.
NOT NULL added directly
Locks the table on big data. Use NOT VALID + VALIDATE for zero downtime.
Over-indexing
Every index slows writes. Add in response to slow queries, not anticipation.
Managed vs self-hosted
Managed by default. Self-hosted only when there's a specific reason: cost at scale, custom extensions, or data residency requirements.
AWS RDS
ManagedManaged Postgres, Multi-AZ, automated backups. The safe default.
AWS Aurora
ManagedPostgres-compatible, better performance at scale, higher cost.
Supabase
ManagedManaged Postgres with auth, storage, and realtime built in.
Neon
ManagedServerless Postgres with branching. Great for preview environments.
DigitalOcean
ManagedSimple managed Postgres. Great price/performance for mid-scale.
Self-hosted
Self-hostedEC2, Hetzner, or on-prem. For cost at scale, custom extensions, or data residency.
Real-world use
Six categories of production systems we've shipped on Postgres.
SaaS backends
Multi-tenant data, RBAC, audit trails, tenant isolation via RLS.
Fintech
ACID transactions, numeric for money, comprehensive audit logging.
Analytics warehouses
Window functions, materialized views, column-store extensions.
Geospatial apps
PostGIS for distance queries, geofences, and location-aware search.
AI features
pgvector for embeddings, RAG pipelines, similarity search alongside relational data.
Time-series
Partitioned tables and TimescaleDB for metrics, events, and telemetry.
Versions
| Version | Status | Our use |
|---|---|---|
| PostgreSQL 16 | Current | Default for all new projects |
| PostgreSQL 15 | Supported | Existing projects — upgrade recommended |
| PostgreSQL 14 | Supported | Legacy — plan migration |
| PostgreSQL 13 and older | End of life | Upgrade required |
When to use Postgres. When not to.
Choose Postgres when
- ACID correctness matters for your domain
- You have complex queries — joins, windows, CTEs
- JSONB can replace a separate document database
- Extensions are useful (pgvector, PostGIS, pg_trgm)
- You want one database instead of three
Consider alternatives when
- Extreme write throughput (Cassandra, ScyllaDB)
- Simple key-value at massive scale (Redis, DynamoDB)
- Billion-scale vector search (Pinecone, Weaviate)
- Embedded only, no server (SQLite)
- You need horizontal sharding by default (CockroachDB, Vitess)
Process
Five phases. Every Postgres engagement we run follows them.
Discovery
Schema design, access patterns, expected scale. We map the top 20 queries before we touch DDL.
Deliverables
Data model · Query inventory · Scale targets · Fixed quote
Schema & migrations
Tables, indexes, constraints. Every migration versioned, reviewed, and tested.
Deliverables
Schema design · Migration files · Rollback strategy · CI integration
Performance baseline
pg_stat_statements enabled from day one. Real EXPLAIN plans for the queries that matter.
Deliverables
Baseline metrics · Slow query log · Index plan · Tuning report
Operations
Backups, replication, monitoring, failover. Everything documented and drilled.
Deliverables
Backup pipeline · Replication setup · Monitoring · Failover runbook
Support
Tuning, version upgrades, feature iteration, on-call. Postgres evolves — we stay current.
Deliverables
Monthly retainer · Upgrade plan · On-call rotation · Quarterly review
Selected work
Postgres systems in production.
Multi-tenant platform
500 tenants on a single cluster. Row-level security, tenant-scoped indexes, read replicas for analytics.
Outcome
500 tenants
Payments backend
ACID transfers, numeric money columns, audit trail on every state change. PCI-aligned schema.
Outcome
Zero lost txns
Semantic search with pgvector
4 million embeddings in HNSW indexes. Hybrid search — vector similarity plus full-text ranking.
Outcome
4M embeddings
Client feedback
What our clients
actually say.
“The move to SikaNet CBS was a bigger shift than I expected — but the right one. Before, our Susu collectors were on paper and every BOG report took us days to assemble. Now collections appear in the office before a collector even finishes their route, and the reports are just there. The team understood our business from day one.”
David Awuni
Founder, Adwumapa Microfinance
“We were cautious about moving patient records online. The on-prem approach changed that — everything stays inside the health center, nothing goes to a foreign cloud. Our clinicians now retrieve a file in seconds instead of ten minutes, and every entry has an audit trail behind it.”
Medical Director
Sekyedumase Health Center
“The patient management system they developed has streamlined our ophthalmology department. Appointment scheduling, patient records, and referral tracking are now seamless — saving us hours of administrative work every day.”
Isaac Adu
Ophthalmologist, Ghana Health Service
Let us talk
Working with PostgreSQL?
We can help — schema design, performance tuning, or managed operations.
