One hard dependency: a PostgreSQL connection string
How we designed an ambitious platform so it starts on a modest server — no Docker, no Redis, no queue.
On paper, WeMovA is a four-storey platform: network, marketplace, payments, intelligence. The kind of roadmap that calls for a "serious" architecture: Kubernetes, Redis, Kafka, an object bucket, three databases.
We made the opposite decision. The platform has exactly one mandatory dependency: a PostgreSQL connection string. Everything else is optional.
Where the constraint comes from
Not from a taste for simplicity. From what the ground looks like.
The first deployment of an African product does not happen on a managed cluster at 400 dollars a month. It happens on a 10-dollar VPS, sometimes on a machine in an office, with a connection that drops. If the architecture needs six services to boot, it does not boot — or it boots into a state nobody knows how to debug at 10 pm on a Sunday.
A hard constraint set on day one beats an optimisation made later. Later, there is code everywhere that assumes Redis.
What PostgreSQL does instead of the others
Almost everything, really, as long as the traffic is that of a product starting out.
Cache: a table with a key, a JSON value and an expiry. Slower than Redis, plenty for profiles and lists.
Queue: a jobs table and SELECT … FOR UPDATE SKIP LOCKED. Several workers can consume without stepping on each other, transactionally. It is more reliable than most lightweight queues, because the job and the data it modifies live in the same transaction.
with next_job as (
select id from jobs
where status = 'pending' and run_at <= now()
order by run_at
for update skip locked
limit 1
)
update jobs set status = 'running', started_at = now()
from next_job where jobs.id = next_job.id
returning jobs.*;Search: native full-text first. Vector similarity with pgvector next, when it is installed — and only then. The code checks for the extension at startup and falls back to full-text otherwise.
Files: on disk to begin with, behind a storage interface. The object bucket comes when there are files to put in it, not before.
Optional, not absent
The nuance matters. Redis, pgvector, object storage: each is planned, each has an interface, each has an adapter. What is forbidden is that any one of them be required for the platform to work.
Concretely, the configuration has exactly one mandatory variable. Every other missing variable triggers a documented fallback, and the startup log says which.
[startup] database: connected (postgres 16)
[startup] cache: postgres table (REDIS_URL not set)
[startup] search: full-text (pgvector extension not found)
[startup] storage: local disk ./data (S3_BUCKET not set)Nobody discovers in production that a piece is missing. The system says what it does, and does it anyway.
What you lose, and when you get it back
You lose cache performance and search latency, from a few thousand active users. That is the right moment to add Redis: by then you have users, hence revenue, hence an infrastructure budget, and above all real measurements to know what to optimise.
Adding a dependency at that point is an operations decision, made with numbers. Removing one when it is all over the code is a rewrite.
The rule I took from it
Every infrastructure dependency must answer one question: what happens if it is not there? If the answer is "it does not start", either make it optional or be certain it will exist everywhere the product will run. For a product aimed at a continent with uneven infrastructure, the first option is the only honest one.
Read next
A payment system starts with a ledger, not an API
Why double-entry bookkeeping is not an implementation detail when you move other people's money.
Isolating several companies inside a single database
Row Level Security in PostgreSQL: the superuser trap, and why an application-level filter is not enough.
FastAPI in production: what actually matters
Five settings you won't find in any tutorial, and that separate an API that runs from an API you can operate.