Every preview-environment demo ends at the app URL. A pull request opens, ninety seconds later a green checkmark appears, and the reviewer clicks through to a live copy of the branch. What the demo never lingers on is where that preview's data came from — which database it is talking to, how that database got its rows, and what happens to it when the PR closes.
That omission is not an accident of demo pacing. Northflank's May 2026 roundup of Railway preview-environment alternatives documents it in the open: its own comparison matrix is fluent about app provisioning and suddenly evasive on the database row, where Coolify and Fly.io "require custom configuration" and Render "requires Preview Environment Initialization." The database is the part nobody demos because, for most platforms, there is nothing to demo — it is your homework. This post fills in that row properly, shows why it decides whether per-PR environments are actually shippable, and prices the graveyard of forgotten previews that forms when teardown is an afterthought.
The matrix, filled in: who provisions the preview database, and who destroys it
Here is the comparison Northflank's post gestures at, with the database column completed from each platform's documented behavior:
| Platform | How a preview gets its data | Seed / restore mechanism | Who destroys it, and when |
|---|---|---|---|
| Northflank | Automatic database clone from production into the preview | Fork via backup and restore, orchestrated with services and jobs | Idle detection, schedule-based cleanup, and duration limits on top of PR-close teardown |
| Render | Fresh database instances created from the Blueprint | Preview Environment Initialization scripts you write and maintain | Auto-delete on merge/close, plus configurable expiry after inactivity |
| Coolify | Nothing by default — previews are application-only | Custom configuration: you wire a database service yourself | Preview app deleted on merge/close; anything custom you added follows your own rules |
| Fly.io | Nothing by default — the review-app workflow deploys one app | Custom workflow config: postgres:/mpg: attach creates a per-app database in a staging cluster; Redis and friends are extra apps | GitHub Actions workflow destroys the review app when the PR closes |
Three cells in that table deserve unpacking, because each hides a design decision that shapes everything downstream.
Coolify's previews stop at the app boundary. Preview scoping (is_preview) is an application concept — databases and services in Coolify have no PR lifecycle at all. Coolify gives you scoped environment variables, wildcard PR domains, and automated PR status comments, and then the database is genuinely your project: provision a service, point the preview at it, and decide its lifecycle yourself. For a single-box self-hoster this is honest minimalism, but it means per-PR data isolation is something you build, not something you get.
Fly.io's review apps are a workflow you own, not a feature you toggle. The superfly/fly-pr-review-apps action deploys one app per PR by default. Attaching Postgres creates a new database named after the review app inside a cluster you designate — the docs steer production users toward a dedicated staging cluster — and every additional store is another app with a unique name in your workflow YAML. Flexible, and destroy-on-close is built in, but fork-from-production seeding and idle shutdown are lines you write.
Render and Northflank both automate teardown; only Northflank forks production data. Render's model is clean but explicit: the Blueprint spins up fresh instances, your Initialization script seeds them, expiry reaps them after inactivity. Northflank is the one vendor in this matrix that treats "clone production into the preview" as a platform primitive via backup-and-restore forking, with idle detection and scheduled cleanup as cost controls. Whether that justifies a vendor's managed control plane versus your own fleet is a separate question — but the shape of the answer (fork, seed, reap) is the same shape a self-hosted platform has to implement.
(For context on the post being alternatived: Railway's PR environments duplicate services from a base environment, and Northflank's piece leans hard on Railway's incident record since late 2025 — a December 2025 EU West episode that paused builds across plan tiers, and a May 2026 platform-wide outage of roughly eight hours after Google Cloud suspended Railway's production account. Preview environments that go dark mid-sprint are their own argument for owning the substrate.)
Why the database decides whether namespace-per-PR is shippable
A self-hosted fleet can absolutely do namespace-per-PR: a controller watches for pull requests, stamps out a namespace, deploys the branch, and wires DNS. Teams do this on raw Kubernetes all the time. The reason so many of those implementations stall at "works in the demo, unused in practice" comes down to three database failure modes.
Seed time is the wall. The naive path — pg_dump from production, pg_restore into the preview, run migrations — scales with database size, and production databases are big. Restoring tens of gigabytes per PR turns "preview in ninety seconds" into "preview sometime after lunch," and reviewers simply stop opening previews that are not ready when the PR is.
The fix is copy-on-write database branching: thin clones that provision in seconds regardless of size because only deltas consume storage. PostgresAI's DBLab 4.0 numbers put a 1 TiB clone at about ten seconds, and the DBLab docs clock a 10 TiB clone at under two seconds for a single user — up to thirty seconds with fifteen concurrent cloners — with dozens of thin clones running on one mid-size machine. The managed world converged on the same primitive: database branching that spins a branch per pull request, including expiring branches for temporary environments, with the per-PR wiring as a standard GitHub Actions pattern. Seed time is a solved problem, but only if you pick the branching primitive instead of the dump pipe.
Realism is the trilemma. PostgresAI's DBLab 4.0 announcement frames the compromise teams actually make, and it rings true: share one staging database and PRs conflict with each other; deploy small synthetic seed databases and you miss exactly the migration bugs and performance cliffs previews exist to catch; clone full production databases and each preview costs hours and real money. Two of the three options make previews untrustworthy, and the third makes them unaffordable — which is why "just point previews at staging" quietly becomes the permanent architecture, and preview environments quietly stop testing anything the staging deploy does not already cover.
Isolation is the missing lifecycle. Per-PR isolation means the preview's data has the same birth and death as the PR itself. Coolify's app-only preview scope shows what happens without it: the application gets a lifecycle and the database gets a hope. Fly.io's attach model is closer — a fresh database per review app, destroyed with it — but isolation sits at the database-name level inside a shared staging cluster, so a runaway preview can still implicate its neighbors' shared fate. True per-PR isolation is a branch or thin clone per PR, created by the same event that creates the preview and destroyed by the same event that destroys it. Anything less is a shared database with extra steps.
The graveyard math: what idle detection plus scheduled cleanup actually saves
Now price the failure mode. Take a team running twenty concurrent previews — unremarkable for a mid-size org with a healthy PR culture. Each preview is cheap in its stateless parts: an app container, some CPU and RAM while it serves traffic. The expensive parts are the stateful ones: the attached volume holding the preview database, and the database process keeping it warm.
Without teardown automation, those stateful parts outlive the PRs they were created for. PRs merge on Friday; their previews idle through the weekend. Long-lived PRs sit for weeks. Reviewers open a preview once and never return, and nothing notices.
This is the graveyard: forgotten previews with attached volumes, each one a small standing bill, multiplying until they outnumber the PRs anyone still remembers. The app containers are not the problem — they are cheap and usually the first thing any cleanup handles. The volumes and warm databases are the cost line that compounds, because storage is metered whether or not anyone queries it, and a running database holds memory whether or not anyone connects.
Map the platforms' teardown controls onto that graveyard and the tiers separate cleanly:
- PR-close deletion only (Coolify, Fly.io): handles the merged-PR case and nothing else. A PR open for three weeks holds its preview resources for three weeks. Stale-but-open is invisible.
- PR-close deletion plus inactivity expiry (Render): adds a backstop. Previews nobody touches get reaped after the configured window even if the PR is still open.
- Idle detection plus scheduled cleanup plus duration limits (Northflank): treats previews as schedulable resources — shut down off-hours, restart on demand, hard-cap lifetime. For the Friday-merge graveyard and the idle-weekend case, scheduled shutdown is the control that actually moves the bill.
The exact savings depend on your PR cadence and volume sizes, but the shape is worth stating plainly: teardown automation does not save the cost of previews you use. It saves the cost of previews nobody is using — which, on any team where opening PRs is easier than closing them, is most of them. The self-hosted equivalent is a reaper with the same three triggers: destroy on PR close, expire after inactivity, and shut down on schedule. Kubernetes gives you the primitives (owner references, TTL controllers, cronjobs); what it does not give you is the opinion that previews are ephemeral by default. You have to install that opinion yourself.
The self-hosted recipe that closes the gap
Nothing in the matrix above is exclusive to managed control planes. A self-hosted fleet can implement the full shape — fork, seed, reap — from composable parts:
- Namespace per PR, owned by a controller. Watch the forge for PR events, stamp out a namespace with the branch deployed, and set owner references so PR close cascades into deletion. This is the part every platform demos, and it is genuinely the easy part.
- Copy-on-write database branching as the data primitive. Run thin-clone branching (DBLab-style on ZFS, or a branching provider behind your own API) so each preview namespace gets a production-scale clone in seconds. PostgresAI's Coolify walkthrough is the existence proof that this composes even with single-box tooling: Coolify handles the app preview, DBLab handles the per-PR Postgres clone, GitHub events drive both, and PR close cleans up both sides.
- Destroy on PR close, both sides. The preview namespace and its database branch die in the same event handler. Coolify's app-only lifecycle is the cautionary tale for wiring only half of this.
- A reaper with idle and scheduled triggers. Expire previews after inactivity, shut them down off-hours, and hard-cap preview lifetime. This is the control that kills the graveyard, and it is the one most DIY preview setups never build — which is exactly why the graveyard is the default outcome.
The through-line: preview databases are not a storage problem, they are a lifecycle problem. Provisioning in seconds is table stakes, production-fidelity data is the point, and automated teardown is what keeps the bill proportional to the PRs anyone still cares about. Any platform — managed or self-hosted — that answers all three has a preview story. Any platform that answers only the app half has a demo.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Per-PR preview environments with production-like data are exactly the kind of lifecycle automation a self-hosted PaaS should own rather than rent. Star the repo on GitHub or deploy your first app today.



