Skip to content

Baseway: Database-Per-Tenant Managed Postgres for EU SaaS Teams

Published: at 09:30 AM

Baseway header

Baseway is managed PostgreSQL for SaaS teams that give every customer their own database. It runs database-per-tenant fleets on Hetzner metal, restores a single tenant to any second with pgBackRest, puts a PgBouncer pooler on every instance, and keeps every byte inside the EU. The tagline on the site says it plainly: the managed Postgres Hetzner never had.

This post explains the problem it solves, how the architecture actually works, and who should be using it.

Table of contents

Open Table of contents

The multi-tenancy problem nobody warns you about

Every B2B SaaS eventually has to answer one question: where does tenant data live?

There are three usual answers, and each one has a tax.

ModelIsolationNoisy-neighbour riskPer-tenant restoreOperational cost
Shared tables + tenant_idWeakest — one bad WHERE clause leaks dataHighEffectively impossibleLowest
Schema-per-tenantMediumHighAwkwardMedium
Database-per-tenantStrongestContainedNaturalHighest — until now

Most teams start with a tenant_id column because it is the cheapest thing to build. Then an enterprise prospect asks for a signed answer on data isolation, or a customer deletes three months of records by accident and wants their data back — not a full-cluster rollback that would clobber everyone else — and suddenly the cheap model is the expensive one.

Database-per-tenant fixes all of that. Isolation is enforced by Postgres itself rather than by your ORM. A tenant’s backup is genuinely their backup. DROP DATABASE is a complete, auditable offboarding.

The reason people avoid it is that running the fleet is miserable. A hundred tenants is a hundred databases to provision, monitor, back up, pool connections for, and restore individually. That is the job Baseway takes over.

What Baseway actually is

The unit of the system is a cell: one PostgreSQL 17 instance on Hetzner metal, holding many tenant databases, with one Kubernetes namespace per organisation.

The key property is that adding a tenant is a row, not a migration. You call the control plane, it places the tenant on a cell, and you get a connection string back. No Terraform run, no new instance to name, no new thing to monitor.

Underneath, Baseway is deliberately built from parts you can reason about: Hetzner Cloud, PostgreSQL 17, pgBackRest, PgBouncer, k3s, Prisma, BullMQ, Ansible, Caddy, Better Auth, and Vector. There is no proprietary storage layer and no exotic consensus protocol. If you have operated Postgres before, you already know most of this stack — which matters a great deal on the day something breaks.

Per-tenant point-in-time recovery, down to the second

This is the feature that justifies the whole architecture.

pgBackRest does continuous WAL archiving with a stanza per cluster, and Baseway verifies the repository is healthy before it touches anything. When you ask for a restore, it runs in two stages:

1. Restore the cell to a scratch location.

pgbackrest --stanza=$CLUSTER_ID info

pgbackrest --stanza=$CLUSTER_ID \
  --type=time \
  --target="2026-08-23 14:32:00+02" \
  --target-action=promote \
  --pg1-path=/scratch/restore \
  restore

The live cell is never touched. What comes back is the whole instance, rewound to your target time, sitting somewhere harmless.

2. Extract the single tenant and reload it.

pgBackRest restores instances; what you actually want is one database. Baseway closes that gap: it pulls the one database out of the scratch copy and reloads it, so the tenant who made the mistake gets their data back and nobody else notices anything happened.

That second stage is the part teams underestimate when they plan to “just use pgBackRest ourselves.” The tooling gives you an instance. The customer asked for a database.

Worth noting how this procedure came to exist: it was run by hand against production first and written down as it actually ran, and the automation is held to that document. That is the opposite of the usual order, and it is why the runbook matches reality.

Connectivity: a pooler on every cell, a control plane that never dials in

Three design decisions here are worth calling out, because they are the ones that tend to bite people who roll their own.

PgBouncer per cell. A pooler sidecar sits beside every instance. Postgres connection slots are a finite, expensive resource, and without pooling you spend them on framework overhead instead of on tenants. Putting the pooler on the cell rather than in a central tier means one tenant’s connection storm cannot starve an unrelated fleet.

Agents dial out only. Data servers long-poll the control plane over HTTPS using per-server bearer tokens. The control plane never opens a connection to a data server. Your database hosts need no inbound ports and no public address — which removes an entire category of exposure from the threat model.

Verified TLS to the tenant. Endpoints look like prefix.region.baseway.tech on port 5432, routed by SNI to the cell holding the database, with verify-full supported properly:

psql "postgresql://app:$PGPASSWORD@wispy-meadow-4f2ab91c.fsn1.baseway.tech\
/tenant_8412?sslmode=verify-full&sslrootcert=system"

psql (17.11)
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384)
tenant_8412=> select count(*) from invoices;
 count
-------
  4128
(1 row)

You pick the tenant with the database name. Nothing else in the connection string changes.

Importing an existing Postgres database

You do not have to start green-field. Paste a connection string and Baseway runs a seven-step, checkpointed import: measure the source, size the cluster from that measurement, create the cell, create the target databases, start the copy, watch it, finalise.

Two details are worth knowing before you run one:

That second point is a good example of the product’s temperament. It would be easy to describe an import as a migration and let customers discover the difference in production.

Observability: numbers the system can prove

Baseway has an engineering convention that I wish more infrastructure products had:

Never show the customer a number the system cannot prove. A stale number presented as current is the same failure as an invented one.

In practice that means CPU and memory are sampled from the cell itself and timestamped, degrading to “how long ago this was seen” when sampling stops, instead of quietly showing you last hour’s figure as if it were live. Every state change is written to an activity log against the organisation, the actor, and the command that caused it. And where the system cannot prove a count, the dashboard says so rather than rendering a confident zero.

The same idea runs through the data model. Baseway stores desired state (what you asked for) separately from observed state (what the agent last saw). “Degraded” is never written down anywhere — it is the computed disagreement between the two. A status field that can drift from reality is a status field that will eventually lie to you.

The correctness work behind it

Managed database products live or die on what happens when something fails midway. Baseway’s approach is that multi-step operations are sagas: a state machine with checkpointed steps, compensations, and per-step timeouts. Agents replay commands, sagas replay steps, and everything is idempotent — so nothing double-provisions.

The public site lists what was actually tested, with the evidence, rather than adjectives:

That last one is a nice piece of operational thinking. Every additional stateful system in a managed-database product is another thing that can be down while you are trying to fix a customer’s database. Running the job queue on the Postgres you already operate removes a dependency instead of adding one.

EU-only infrastructure and data residency

Everything runs on Hetzner in the EU, and nothing leaves the EU. For European SaaS companies this is not a nice-to-have — it is the line item in the security questionnaire that decides whether the deal closes.

Combined with database-per-tenant isolation, it gives you an unusually strong answer to the two questions enterprise buyers actually ask:

  1. Where is our data? — In the EU, on named infrastructure, with a published subprocessor list.
  2. Is it mixed with other customers’ data? — No. Separate database, separate credentials, separate backup, separate restore.

Answering both of those without hand-waving is worth real money at the point of sale.

Pricing

Baseway charges flat pricing rather than metering you per query, per connection, or per compute-second. The appeal of flat pricing for a database-per-tenant fleet is obvious: your cost scales with the capacity you provisioned, not with a usage curve you cannot predict at the point you sign the customer. Current figures are on baseway.dev.

What is on the roadmap

Two pieces of feasibility work are done and documented but not shipped yet — flagging them because the difference matters:

Who Baseway is for

Baseway is a good fit if you are:

It is probably not for you if you have one shared database with a tenant_id column and no intention of changing that — the architecture’s advantages simply do not apply.

Try it

You can spin up a cluster on Hetzner, import an existing database from a connection string, and restore any tenant to any second at baseway.dev.

If you enjoy this kind of infrastructure writing, you might also like my posts on self-hosting a reverse proxy and self-hosting an LLM on RunPod.