Two-thirds of enterprise teams now name ephemeral environments a stated 2026 investment priority — not an experiment, a budget line. A market roundup this year put Bunnyshell, Northflank, Uffizzi, Okteto, and Humanitec at the front of that spend, each selling "spin up a full isolated deploy per pull request, tear it down on merge" as a distinct product.
Here's the part that roundup doesn't say: strip the pattern down to what it actually requires underneath, and a Cluster-API-managed, git-push PaaS already has two of the three pieces built — because they're the same pieces a normal git-push deploy needs. The third piece is a real gap, not a rounding error, and it's worth being precise about which is which before paying for a bolt-on layer to get it.
What a preview environment actually requires
Under the marketing, "preview environment" decomposes into three concrete requirements:
- Fast per-branch build-and-deploy — a push to a PR branch produces a running deploy, not just a build artifact, before the reviewer loses context.
- A DNS/TLS story that doesn't need manual provisioning per environment — every PR gets its own routable HTTPS URL, and nobody files a ticket to get one.
- A teardown trigger tied to PR close — the environment dies when the PR does, not when someone remembers to clean it up.
Two of these are things a Cluster-API-managed PaaS's existing git-push loop already does for every ordinary deploy. The third is genuinely new work. Below is what each looks like in practice, and where the line actually falls.
Requirement 1: fast per-branch build-and-deploy
A git-push PaaS already carries a branch or commit ref through its deploy pipeline — that's how staging and production deploys off different branches work today. Making a PR branch deploy to its own environment instead of overwriting an existing one is a routing change, not a new subsystem: the webhook handler that normally resolves refs/heads/main to "redeploy the App" instead resolves refs/heads/feature-x to "create a new App, scoped to this ref."
A branch-scoped App custom resource looks like an ordinary deploy with two extra fields — the PR number and the source ref:
apiVersion: bex.co/v1
kind: App
metadata:
name: checkout-pr-482
labels:
bex.co/pr: "482"
bex.co/repo: acme/checkout
spec:
source:
repo: https://github.com/acme/checkout
ref: refs/pull/482/head
build:
strategy: buildpacks
scaling:
minReplicas: 1
maxReplicas: 1The reconcile loop that turns this into a running Deployment + Service is the exact controller already running for production App objects — it doesn't know or care that ref points at a PR head instead of main.
The number that actually matters here is build latency, and it isn't one number — it's a range that moves with two variables: layer-cache state and monorepo fan-out. A single-service Node or Go app with a warm buildpack cache (base image and dependency layers already resolved from the previous commit on that branch) typically lands in the 30–90 second range for build-plus-rollout. A cold cache — first push on a brand-new branch, or a base-image bump that invalidates every layer — pushes that to 3–5 minutes.
A monorepo where the PR touches a shared package used by four services multiplies the cold case, since each dependent service rebuilds from its own now-invalid cache; that's the scenario where a 90-second single-service preview turns into an 8–10 minute one. None of this needs new infrastructure — it's the same buildpack cache warming that already makes a repeated main deploy fast. It just means "how fast is my preview" is really "how fast is my normal deploy," and teams with slow production builds will get slow previews too.
Requirement 2: DNS/TLS with zero manual provisioning
This is the part dedicated preview-environment vendors advertise hardest, and it's the part a custom-domain-capable PaaS gets closest to for free. The pattern is a wildcard DNS record plus an ACME DNS-01 wildcard certificate, both provisioned once at the platform level, not once per environment:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: preview-wildcard
spec:
secretName: preview-wildcard-tls
dnsNames:
- "*.preview.acme-checkout.bex.app"
issuerRef:
name: letsencrypt-dns01
kind: ClusterIssuerOne wildcard cert, one *.preview.acme-checkout.bex.app DNS record pointed at the ingress, and every new App — PR 482, PR 483, PR 12,004 — gets pr-482.preview.acme-checkout.bex.app for free the instant its Ingress object exists. No per-PR certificate request, no per-PR DNS API call, no waiting on ACME's rate limits (which bite hard if you're requesting one cert per PR instead of one cert total). This is the same mechanism that already backs custom-domain support for a paying tenant's production app — a platform that's solved "give a tenant a working HTTPS URL without a human touching DNS" has already solved this requirement; it's a naming-pattern decision, not new plumbing.
Requirement 3: teardown tied to PR close
This is the one piece that isn't already sitting in a normal deploy pipeline, because a normal deploy pipeline has no concept of "this deploy should stop existing." It needs a new webhook subscription and a small controller, not a bolt-on platform:
# GitHub webhook payload on PR close (merged or not)
{
"action": "closed",
"pull_request": { "number": 482, "merged": true },
"repository": { "full_name": "acme/checkout" }
}func (r *PreviewReconciler) OnPullRequestClosed(ctx context.Context, repo string, prNumber int) error {
return r.client.DeleteAllOf(ctx, &bexv1.App{},
client.MatchingLabels{"bex.co/pr": strconv.Itoa(prNumber), "bex.co/repo": repo},
)
}That's genuinely all it is — a webhook listener and a label selector delete — but it's the piece that turns "we can deploy a branch" into "we have a preview-environment feature, without zombie deploys accumulating." Skip it and you get exactly the failure mode ephemeral-environment vendors use in their own sales pitch: environments that outlive their PRs, quietly billing compute nobody's using until someone audits the cluster and finds forty stale namespaces from closed PRs three months old.
The honest gap: isolation and scoped secrets at N concurrent environments
Two of three requirements reuse the existing git-push loop; the third is one small controller. So where's the real gap? Two places, and it's worth being specific instead of waving at "isolation" as a vague concern.
Resource contention at scale. A namespace-per-PR pattern (each App in its own namespace, with a ResourceQuota capping CPU/memory) is native to any Kubernetes-based PaaS and gives soft isolation — one PR's environment can't starve another's pod scheduler. What it doesn't give is hard tenant isolation: all those namespaces still share one cluster's node pool, one API server, one etcd. At low PR volume — a handful of concurrent previews — that's a non-issue.
At the volume a mid-size engineering org actually generates — 40-plus open PRs on a busy week, each with its own preview App — a noisy build job in one namespace (a monorepo cold-cache rebuild, say) can still starve node-level CPU for pods scheduled alongside it, because ResourceQuota bounds a namespace's aggregate requests, not real-time contention on a shared node. This is exactly the gap Uffizzi's architecture is built to close: it gives every environment its own lightweight virtual Kubernetes cluster (via vcluster), trading some overhead for hard control-plane-level isolation instead of soft quota-based isolation. A Cluster-API PaaS can get there too — vcluster composes with Cluster API rather than competing with it — but it's additional machinery, not something the existing git-push loop already includes.
Environment-scoped secrets. Production and staging App objects pull from platform-managed secret stores scoped to an environment name — production, staging. A preview environment needs the same pattern scoped to a PR number instead, with a default that's actually safe: a PR from an external contributor's fork should not inherit the same database credentials or third-party API keys production uses, deliberately or by accident. That's a policy decision (a bex.yml preview.secrets: inherit-staging vs. preview.secrets: none default) a platform has to make and enforce, not something that falls out of the existing build pipeline for free.
Where that leaves the five vendors
| Platform | What it sells as differentiated | Native to a Cluster-API git-push loop? |
|---|---|---|
| Bunnyshell | YAML-defined environments from Compose/Helm/Terraform, PR-triggered | Build+deploy: yes. Definition format: adds a DSL on top. |
| Northflank | Managed databases provisioned per preview, BYOC across clouds | DB-per-preview: not native — needs a database-provisioning layer |
| Uffizzi | Per-environment virtual cluster (vcluster) for hard isolation | Hard isolation: genuine gap, as above |
| Okteto | Live code sync into a running preview cluster | Live sync: a dev-loop feature, orthogonal to preview-per-PR |
| Humanitec | Resource-graph-driven orchestration across any compute, AI-agent-first API | Cross-cloud abstraction: out of scope for a single-cloud fleet |
Read the table the other way and the pattern is clear: every one of these vendors' headline differentiators sits on top of the three baseline requirements, not inside them. Strip the baseline out — fast per-branch deploy, free-per-PR TLS, PR-close teardown — and what's left is managed databases, virtual-cluster isolation, live sync, and cross-cloud abstraction: real features, but layered on a foundation a git-push PaaS already has two-thirds built.
Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a Render-compatible API that treats "deploy this PR branch" as an ordinary deploy call, not a separate product tier. It doesn't sell managed databases or a closed SaaS on top — it composes with whatever ephemeral-environment tooling a team already trusts for the parts that genuinely need it. Star the repo on GitHub or deploy your first app today.



