Innov8ProTech
PostgreSQLTechnologies / PostgreSQL

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 for new projects
Managed or self-hosted
Zero-downtime migrations

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.

ACID compliance — no exceptions
Rich data types — jsonb, arrays, ranges, inet
Extensions — a whole ecosystem beyond SQL
Window functions and CTEs — complex queries without application code
MVCC concurrency — no readers block writers
Logical replication — partial, transformable, cross-version

Postgres vs others

Every database is a set of trade-offs. Here's when we pick Postgres, and when we don't.

PostgreSQL

Default

Type

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.

TypeUseNote
uuidPrimary keys, external IDsGenerated with gen_random_uuid()
jsonbFlexible payloads, event dataIndexable, queryable — unlike json
text[]Tags, permissions, small listsNative array, indexed with GIN
timestamptzEvery timestamp, alwaysNever use timestamp — timezone bugs
numericMoney, precise decimalsNot float — no rounding surprises
inet / cidrIP addresses, CIDR rangesNative validation, indexing
tsvectorFull-text searchBuilt-in, no separate search engine
rangeDate ranges, booking windowsOverlap constraints built in
intervalDurations, retention windowsFirst-class time arithmetic
vectorEmbeddings (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.

Observability

pg_stat_statements

Query performance tracking. First thing we install on any production DB.

Security

pgcrypto

Encryption, hashing, and UUID generation. Used for sensitive columns.

Search

pg_trgm

Trigram similarity. Fast ILIKE queries, fuzzy search, autocomplete.

Geospatial

postgis

Geospatial. Points, distances, geofences — used in logistics and field apps.

Partitioning

pg_partman

Table partitioning automation. For time-series and event tables at scale.

AI

pgvector

Vector similarity. Embeddings, RAG, semantic search — AI features.

Automation

pg_cron

Scheduled jobs inside the DB. Vacuum, materialized view refresh, custom tasks.

Maintenance

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

TypeScript

Type-safe, migrations built-in, great DX. Our default for TS projects.

Drizzle

TypeScript

SQL-like syntax, minimal abstraction. For teams that want to see the SQL.

SQLAlchemy

Python

The Python standard. 2.0 syntax is excellent — typed and composable.

Eloquent

PHP

Laravel's ORM. Great for CRUD, less so for complex analytics.

Raw SQL

Any

For complex reports, aggregations, and anything performance-critical.

Mix and match

Any

ORMs 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

Managed

Managed Postgres, Multi-AZ, automated backups. The safe default.

AWS Aurora

Managed

Postgres-compatible, better performance at scale, higher cost.

Supabase

Managed

Managed Postgres with auth, storage, and realtime built in.

Neon

Managed

Serverless Postgres with branching. Great for preview environments.

DigitalOcean

Managed

Simple managed Postgres. Great price/performance for mid-scale.

Self-hosted

Self-hosted

EC2, 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.

Multi-tenantRLS

Fintech

ACID transactions, numeric for money, comprehensive audit logging.

ACIDAudit

Analytics warehouses

Window functions, materialized views, column-store extensions.

OLAPWindows

Geospatial apps

PostGIS for distance queries, geofences, and location-aware search.

PostGISGeo

AI features

pgvector for embeddings, RAG pipelines, similarity search alongside relational data.

pgvectorRAG

Time-series

Partitioned tables and TimescaleDB for metrics, events, and telemetry.

TimescalePartitioned

Versions

VersionStatusOur use
PostgreSQL 16CurrentDefault for all new projects
PostgreSQL 15SupportedExisting projects — upgrade recommended
PostgreSQL 14SupportedLegacy — plan migration
PostgreSQL 13 and olderEnd of lifeUpgrade 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.

01

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

02

Schema & migrations

Tables, indexes, constraints. Every migration versioned, reviewed, and tested.

Deliverables

Schema design · Migration files · Rollback strategy · CI integration

03

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

04

Operations

Backups, replication, monitoring, failover. Everything documented and drilled.

Deliverables

Backup pipeline · Replication setup · Monitoring · Failover runbook

05

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.

SaaS

Multi-tenant platform

500 tenants on a single cluster. Row-level security, tenant-scoped indexes, read replicas for analytics.

Outcome

500 tenants

Fintech

Payments backend

ACID transfers, numeric money columns, audit trail on every state change. PCI-aligned schema.

Outcome

Zero lost txns

AI product

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.

DA

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.

MD

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.

IA

Isaac Adu

Ophthalmologist, Ghana Health Service

FAQ

Postgres
questions.

Still curious?
PostgreSQL by default for new projects — jsonb, arrays, extensions, window functions, and better query planning. MySQL when you need extreme read throughput with a simple schema, or when you're constrained by an existing MySQL-based ecosystem.

Let us talk

Working with PostgreSQL?

We can help — schema design, performance tuning, or managed operations.