Import your existing Postgres database.

See how it works.

The managed Postgres Hetzner never had.

Database-per-tenant fleets on Hetzner metal. Per-tenant point-in-time recovery, flat pricing, and nothing that leaves the EU.

Built on partswe can reason about
Hetzner Cloud
PostgreSQL 17
pgBackRest
PgBouncer
k3s
Prisma
BullMQ
Ansible
Caddy
Better Auth
Vector
Fleets

Database-per-tenant, without running the fleet yourself.

A cell is one Postgres instance on Hetzner metal, holding many tenant databases. You create tenants; Baseway places them, watches them, and tells you the truth about what it sees.

Cells, not snowflakes

One instance, many tenant databases, one namespace per organisation. Adding a tenant is a row, not a migration.

Desired versus observed

We store what you asked for and what the agent last saw. Degraded is never written down — it is the disagreement between the two.

Sagas that replay safely

Every multi-step operation checkpoints, compensates, and is safe to run twice. Agents replay commands; nothing double-provisions.

Recovery

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

pgBackRest underneath

Continuous archiving with a stanza per cluster, and a repository we check is healthy before anything is touched.

One tenant, not the whole cell

We restore the instance to a scratch location at your target time, extract the single database, and reload it. You get one tenant back.

Rehearsed against production

The procedure was run by hand against production first and written down as it actually ran. The automation is held to that document.

Connect a tenant database

Every cluster gets a verified-TLS endpoint. Pick the tenant with the database name.

connect
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)
Import

Bring an existing database. Paste a connection string.

Seven steps, checkpointed

Measure the source, size the cluster from that measurement, create the cell, create the database, start the copy, watch it, finalise.

Survives restarts

The copy runs as a Kubernetes Job in your own namespace, so an agent restart or a host reboot does not lose it.

A snapshot, and we say so

pg_dump into pg_restore. Writes that reach the source after the import starts are not copied, and the dashboard tells you that.

Connectivity

A pooler on every cell, and a control plane that never dials in.

PgBouncer per cell

A pooler sidecar beside every instance, so connection slots stay yours to spend on tenants instead of on overhead.

Agents dial out only

Data servers long-poll the control plane over HTTPS with per-server bearer tokens. Nothing ever opens a connection inward.

Verified TLS to the tenant

Endpoints are prefix.region.baseway.tech on 5432, verify-full, routed by SNI to the cell that holds the database.

Restore one tenant to a point in time

The two stages the restore actually runs in. pgBackRest restores instances; you get back a database.

restore
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 untouched. The scratch copy is the
# whole instance, rewound to the target time.
Observability

Numbers we can prove, carrying the time they were taken.

A stale number presented as current is the same failure as an invented one. Every measurement shows its age rather than implying freshness.

CPU and memory per cell

Sampled from the cell itself and timestamped, degrading to how long ago it was seen when sampling stops.

An activity log you can audit

Every state change is recorded against the organisation, the actor, and the command that caused it.

No invented totals

Where the system cannot prove a count, the dashboard says so instead of showing a confident zero.

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

Proven, not assumed.

Every claim below was observed against a running system and written down with its evidence. Where something is unverified, our docs say so.

The worker boots with no database
Started against a dead catalog. It logged degraded mode and kept ticking rather than exiting.degraded mode held · clean exit on SIGTERM
An agent survives a dead control plane
Killed the control plane mid-poll and watched the agent back off. It never exits; it waits.backoff 1.2s → 1.8s → 3.8s → 7.6s, jittered
Unknown commands never crash an agent
Sent a command the agent has no handler for, then a malformed payload. Both were rejected the same way.failed / unsupported_command · polling continued
Commands are idempotent on replay
Reset a completed command to pending and let it run a second time. The agent adopted the existing container.attempts 1 → 2 · containers still 1
Leases hand out work exactly once
Three queued commands, two workers leasing with a limit of two, then a reclaim after the lease expired.leased 2 → 1 → 0 · reclaimed 3
The sweep is the safety net
Disabled the tick queue entirely and let the periodic sweep carry the saga on its own. No nudge required.0 tick jobs consumed · 5 sweep advances → succeeded
Jobs run on Postgres, with no Redis
Migrated BullMQ onto its Postgres backend, enqueued and processed a job, then grepped the lockfile.0 matches for redis or ioredis
The cluster pipeline works end to end
Created a cluster from the dashboard and connected to the database it provisioned.saga succeeded · cluster running · PostgreSQL 17.11

Ready to move your tenants?

Spin up a cluster on Hetzner, import an existing database from a connection string, and restore any tenant to any second.

Create a cluster