One Demo per Prospect: Sales Environments You Can Throw Away
Every B2B SaaS company has a machine called demo. It has a URL the account execs can type from memory, a login pinned in a Slack channel, and a database that was seeded properly exactly once — during the launch push, some time in 2024. Since then it has been lived in. There is an account called `asdf`. There is another called `asdf2`. There is a workspace named ALL CAPS DO NOT DELETE, created by a person who no longer works there. And there is a support rep's half-finished bug repro, frozen mid-experiment, sitting in the exact list view your AE opens on slide one.
I'm Ajay; I built PandaStack, a Firecracker microVM platform, and demo environments are one of those systems engineering teams inherit rather than choose. Nobody designed a shared mutable production-shaped machine with no owner and no reset. It accreted. This post is about the shape that works instead: bake one golden demo — the app, its database, and realistic data, all running — and fork it per prospect, so a clean environment is a sub-second copy rather than a provisioning job.
The shared demo instance is a haunted house
The failure mode isn't that the shared demo is badly maintained. It's that a shared mutable environment with many writers and no owner has exactly one stable state, and that state is "lightly ruined." Everyone who touches it leaves something behind, nobody has the appetite to delete anyone else's stuff, and the seed script that would fix it drifted three schema versions ago. What accumulates, roughly in the order you'll find it:
- Test accounts named by a human's left hand — `asdf`, `test2`, `aaaaa` — sitting at the top of every sorted list, because someone picked a name that sorts first.
- A support engineer's half-finished repro: a customer edge case recreated at 4pm on a Thursday and abandoned in whatever broken intermediate state it was in.
- Feature flags nobody remembers toggling. Someone needed to check a behavior, flipped it, and the pitch that assumed the other behavior finds out live.
- Leftovers from previous prospects: a workspace named after a company, a record containing a competitor's product names, an uploaded file with someone else's data in it.
- Schema drift, because a migration was run by hand to unblock a call and never made it back into the repo. The demo is now a branch of your product that exists on one box.
- Time rot. Seeded data has fixed timestamps, so the "last 30 days" dashboard is empty and the trend chart your deck calls hockey-stick is a flat line ending eighteen months ago.
That's the comedy. The version that isn't funny is a prospect asking the best question in any sales call — "can we try it with our actual data?" — and uploading a CSV into an instance the next four demos will open.
Why "just spin up a staging copy" doesn't scale
The obvious fix is to give each prospect their own environment using the automation you already have. It produces an environment. It doesn't survive contact with how sales actually runs.
- Provision time. Infrastructure apply, database creation, migrations, seeding, DNS, certificates: minutes at best and often an hour. Fine for a planned POC, useless when an AE needs a clean environment fifteen minutes before a call, or during one.
- Cost shape. An environment used for a 45-minute demo once a week is billed for all 168 hours of that week, and it scales linearly with pipeline — the one number your CFO wants going up.
- Lifetime. POC environments do not get deleted. There is one from March. It belongs to a deal that closed, or didn't; nobody can tell from the tags, and nobody wants to be the person who deleted the wrong one.
- Drift. Environments provisioned on different days sit on different releases with different seed scripts, so "it worked in my demo" becomes an actual engineering investigation.
None of this is your cloud provider's fault, and provisioning behavior and pricing differ by vendor and change over time — verify specifics against their own docs. The structural problem is that you're building a machine each time, when what you want is a copy of a machine you already built and already trust.
The shape: bake a golden demo, fork it per prospect
A PandaStack sandbox is a Firecracker microVM: its own guest kernel, its own disk, its own network namespace and tap device. A snapshot captures that VM whole — memory and disk, mid-flight, processes running. A fork clones a snapshot with copy-on-write memory and a reflinked rootfs, so the new VM starts life sharing pages with its parent and only diverges where it writes.
That changes what a demo environment costs. A same-host fork lands in the 400–750ms range; cross-host, where memory comes from object storage, it's 1.2–3.5s. Creating a sandbox from a template snapshot is p50 179ms and p99 203ms, with the restore step itself around 49ms — only the first-ever cold boot of a template costs about 3 seconds. At those numbers, a demo environment stops being infrastructure you request and becomes a button in your internal tool.
What goes into the golden
The golden demo is the machine you wish the shared instance still was, frozen at its best moment:
- The app at the exact release you're selling, built and already serving traffic — not an image that still has to start.
- Postgres inside the same VM, initialized and running. If you provision a managed database per prospect instead, note that creating one takes 30–90s because it waits for the server to be genuinely ready; doing that once at bake time is most of the win.
- Seed data that looks like a company rather than a fixture file: eighteen months of history, plausible names, realistic activity, and a few deliberately messy rows so the product looks like it survives reality.
- Feature flags in the state your deck assumes, and an authenticated session so the first click is the product and not a login form.
- Warm everything. The snapshot captures memory, so whatever you warmed before snapshotting — page cache, connection pool, a JIT that already compiled the dashboard query — is warm again in every fork. Your prospect's first click is your hundredth click.
Forking one per prospect
The provisioning code is short, because there's almost nothing to provision. You copy a machine, check it woke up sane, and hand back a URL.
import os
from pandastack import Sandbox
# Bumped by CI every time we cut a release and re-bake the golden demo.
GOLDEN = os.environ["DEMO_GOLDEN_SNAPSHOT"]
APP_PORT = 3000
def provision_demo(account_id: str, ae_email: str, days: int = 1) -> dict:
"""One prospect, one microVM, forked from the golden demo snapshot."""
sbx = Sandbox.fork(
GOLDEN,
ttl_seconds=days * 24 * 3600, # it expires; nobody has to remember
metadata={
"kind": "sales-demo",
"account": account_id, # your CRM id, not "northwind (new)"
"owner": ae_email,
"golden": GOLDEN, # exactly which build they saw
},
)
# The app and its Postgres were already running when we snapshotted, so
# there is no boot, no migrate, no seed here. Just confirm it woke up sane.
health = sbx.exec(
f"pg_isready -t 10 && curl -fsS localhost:{APP_PORT}/healthz",
timeout_seconds=30,
)
if health.exit_code != 0:
sbx.kill()
raise RuntimeError(f"golden {GOLDEN} is bad: {health.stderr[-500:]}")
return {
"sandbox_id": sbx.id,
"url": sbx.preview_url(port=APP_PORT),
"expires_in_days": days,
"checked_in_ms": health.duration_ms,
}
if __name__ == "__main__":
# A 14-day POC environment, created while the AE is still on the call.
demo = provision_demo("acct_8812", "dana@example.com", days=14)
print(demo["url"]) # https://3000-<sandbox-id>.<suffix> -- paste into the inviteNote what isn't in that function: no migration, no seeding, no retry loop around a database that isn't accepting connections yet. Those happened once, at bake time, on a build machine, where a failure is a red CI job instead of a red face.
Reset is a re-fork, not a cleanup script
Everyone's first instinct is a reset endpoint: truncate the tables, re-run the seeds, clear the object store, invalidate the caches. It works beautifully the day you write it. Then someone adds a table and forgets the truncate list, and your "clean" demo has orphaned rows from a prospect three weeks ago. Cleanup SQL is a second, worse copy of your schema that only runs on Sundays, which means it's only wrong on Mondays.
Forking makes reset idempotent by construction. You don't clean the machine; you delete it and take another copy of the golden one. The starting state isn't "we believe we removed everything" — it's the same bytes, every time, including the state you forgot existed. Adding a table can't break it, because it doesn't know what a table is.
import os
import datetime as dt
from pandastack import Sandbox
APP_PORT = 3000
PITCH_PATH = ["/", "/dashboard", "/reports/quarterly", "/settings/billing"]
def reset_demo(sandbox_id: str) -> dict:
"""Reset = throw the machine away and take a fresh copy of the golden one."""
old = Sandbox.get(sandbox_id)
meta = dict(old.metadata) # account, owner, golden, ...
ttl = old.ttl_seconds # keep the original expiry, not a fresh one
old.kill() # memory, disk, stray processes: gone
fresh = Sandbox.fork(
os.environ["DEMO_GOLDEN_SNAPSHOT"],
ttl_seconds=ttl,
metadata={**meta, "reset_at": dt.datetime.now(dt.UTC).isoformat()},
)
return {"sandbox_id": fresh.id, "url": fresh.preview_url(port=APP_PORT)}
def rebake_golden(release_tag: str) -> str:
"""Nightly and on release: build the demo once, snapshot it, publish the id."""
sbx = Sandbox.create(
template="base",
persistent=True, # exempt from the idle reaper while we bake
metadata={"kind": "golden-demo", "release": release_tag},
)
steps = [
"DEBIAN_FRONTEND=noninteractive apt-get install -y -q postgresql-16",
"pg_ctlcluster 16 main start && pg_isready -t 30",
f"git -C /srv/app fetch --depth 1 origin {release_tag}",
"git -C /srv/app checkout --force FETCH_HEAD",
"cd /srv/app && npm ci && npm run build && npm run db:migrate",
# Seed data lives in the repo and gets reviewed like any other code.
"cd /srv/app && node scripts/seed-demo.js --profile northwind "
"--months 18 --relative-to now",
"cd /srv/app && setsid npm start > /var/log/demo-app.log 2>&1 < /dev/null &",
]
for cmd in steps:
r = sbx.exec(cmd, timeout_seconds=900)
if r.exit_code != 0:
sbx.kill()
raise RuntimeError(f"bake failed on: {cmd}\n{r.stderr[-2000:]}")
# Walk the exact path the deck walks. This is both a smoke test and the
# reason every fork starts with hot caches instead of a cold first click.
for path in PITCH_PATH:
r = sbx.exec(
f"curl -fsS -o /dev/null -w '%{{http_code}}' localhost:{APP_PORT}{path}",
timeout_seconds=60,
)
if r.exit_code != 0 or r.stdout.strip() != "200":
sbx.kill()
raise RuntimeError(f"pitch path broken at {path}: {r.stdout}")
snap = sbx.snapshot() # memory + disk, app and Postgres running
sbx.kill()
return snap.id # CI writes this to DEMO_GOLDEN_SNAPSHOTTwo honest caveats. A re-fork produces a new sandbox id, so if AEs paste demo links into calendar invites days ahead, front the environment with a stable hostname you re-point on reset. And resetting genuinely destroys whatever the prospect built during the call — that's the point, but say so in your UI before someone loses the workflow they spent twenty minutes configuring.
A demo that sleeps 165 hours a week should cost like it
Do the arithmetic before you commit. A busy AE might use an environment for three hours in a week. There are 168 hours in a week. Under the always-on staging model you pay for 168 and use 3, multiplied by every open opportunity — the number you are actively trying to increase.
Two mechanisms fix the shape. Idle hibernation snapshots the VM and stops it, so an untouched demo costs storage rather than compute, and waking it is a snapshot restore rather than a boot: the AE clicks the link and the machine is back before the page finishes rendering. TTL handles the rest — you set `ttl_seconds` at fork time and a 14-day POC deletes itself on day 14 without a human decision.
Expiry also converts a cleanup problem into a sales conversation, because someone has to ask the prospect whether they still need the environment — a call your AE wanted an excuse to make anyway. The zombie POC from March was never really a technical problem. It was a technical problem you gave a credit card to.
Prospect A's sample data, Prospect B's eyes
There's a reason nobody demos on production, and it isn't only the risk of deleting a real customer's account on a call. A demo is a session where you invite a stranger to click things in an environment you don't fully control. The shared demo instance has the same problem at lower stakes — until a prospect uploads their own data into it.
They will; "can we load one of our real exports?" is the moment an evaluation gets serious. On a shared instance that export now sits in a database the next four demos open, and one of those four may be the uploader's direct competitor. Your buyer's security team asks about this in the questionnaire, usually as: is evaluation data segregated per customer, and what happens to it afterwards?
A microVM per prospect lets you answer structurally instead of procedurally. Each environment gets its own guest kernel, its own disk, and its own network namespace with its own tap device — PandaStack pre-allocates 16,384 /30 subnets per agent, so per-sandbox networking is the default shape rather than a special request. The answer stops being "every query is scoped by tenant id and we're fairly confident we didn't miss one."
- Isolation boundary — one Firecracker microVM per prospect behind hardware virtualization, with a separate guest kernel, rather than shared rows in a shared database.
- Data lifetime — the environment expires on a TTL and the disk is destroyed with the VM. "We deleted the machine" is far easier to evidence than "we deleted the rows, and the backups, and the caches, and the search index."
- Access — an unguessable per-sandbox URL plus your app's own authentication. For a POC holding a real export, don't lean on URL secrecy alone; put a login in front of it.
- Egress — default-deny outbound with an allowlist for the integrations the demo genuinely needs, so a demo environment can't reach your internal services just because it runs on your infrastructure.
This is architecture, not compliance. It gives you good answers to isolation questions; it doesn't substitute for a DPA, a retention policy, or a certification. If a prospect wants regulated data in a POC, that's a contract conversation before it's an infrastructure one — and the cheaper answer is often excellent synthetic data instead.
Keeping the golden from rotting
You've turned the demo environment into a build artifact, which means it needs a build artifact's discipline. This is the part teams skip, and it's the part that decides whether any of this still works in six months.
- Seed data as code. The seed script lives in the repo and is reviewed in pull requests, and it's the only way data gets into the golden. The moment someone fixes the demo by hand-editing the database, you've rebuilt the haunted house with extra steps.
- Re-bake on every release tag, plus nightly. The bake is itself the smoke test: if a migration fails or the seed script breaks against a new schema, you hear it from a red CI job at 3am, not from an AE at 9:05am.
- Test the pitch path, not just the health check. Script the five clicks your deck depends on and fail the bake if any of them stops returning 200. The demo's happy path is a test suite; it's just one nobody has written down.
- Seed timestamps relative to bake time. Fixed dates are why every stale demo has an empty "last 30 days" chart. Generate history relative to now and the problem disappears instead of being managed.
- Keep the previous golden as a rollback, and pin the golden id in each demo's metadata. When today's bake is subtly broken five minutes before a call, you fork yesterday's; when an AE says "it did something weird on Tuesday," you fork exactly what they were using.
- Detect drift the boring way: diff the golden's schema against a fresh migration run, and diff its row-count profile against what the seed script claims it produced. Both are cheap, and both catch the case where the golden and the product have quietly diverged.
Shared instance vs staging copy vs container vs microVM fork
Same job, four topologies. Characterizations of other runtimes and cloud services below are qualitative on purpose — behavior, limits, and pricing differ by version and configuration and they change, so verify specifics against the relevant vendor's documentation before planning around them.
- Provision time — Shared demo instance: zero, it already exists, which is precisely the problem. Per-prospect cloud staging: minutes to hours for infra apply, database creation, migrations, and seeding. Container per demo: fast if the image is cached and the database is empty, but seeding is the slow half and doesn't get faster. microVM fork: 400–750ms same-host and 1.2–3.5s cross-host, from a machine already seeded and already warm.
- Reset cost — Shared demo instance: a cleanup script you maintain forever, half-trust, and forget to update when the schema changes. Per-prospect cloud staging: destroy and re-provision, so you pay the provisioning time again. Container per demo: re-create and re-seed; the container is cheap, the data isn't. microVM fork: kill and re-fork, identical bytes every time, idempotent by construction rather than by discipline.
- Data isolation — Shared demo instance: none worth the name; every prospect's uploads live in one database. Per-prospect cloud staging: genuinely separate, assuming your automation never accidentally shares a subnet, bucket, or database instance. Container per demo: a shared host kernel with namespaces and cgroups, so the boundary depends on your runtime configuration and the kernel's current bug list. microVM fork: separate guest kernel, disk, and network namespace per prospect, behind hardware virtualization.
- Idle cost — Shared demo instance: one always-on box, cheap in absolute terms, but you only get one of them. Per-prospect cloud staging: full price for every environment for all 168 hours a week, used or not. Container per demo: cheaper per environment, though the database and the node underneath it are still running. microVM fork: hibernate between calls so an idle demo costs storage, and TTL deletes POCs on schedule.
- Concurrent demos — Shared demo instance: one, honestly, whatever the calendar claims. Per-prospect cloud staging: as many as budget and provisioning lead time allow, decided days in advance. Container per demo: many, until seeding time and database resources become the bottleneck. microVM fork: many, because each fork is copy-on-write memory and a reflinked rootfs — the marginal demo is mostly bookkeeping, and the ceiling is host memory.
Same machine, other rooms
Once "fork the golden" is a button, the shape covers a surprising amount of the company. Hands-on workshops: thirty attendees, thirty identical environments, and when one person wedges theirs at 10:15 you re-fork it while they're still explaining what happened. Self-serve trials: a real environment with real-looking data instead of an empty tenant and a checklist. Conference booths, where you should assume every visitor types something unrepeatable into your product and you reset between them. Customer training, where every student needs the same starting state and the exercise involves breaking it. Support reproduction: fork a golden matching the customer's version and debug on a machine you're going to delete.
The general principle is that a demo environment is a cache of a known-good state, and the bug was ever letting that cache be mutable, shared, and impossible to invalidate. Snapshot and fork turn it back into what it should have been: a copy you take, use, and throw away.
None of this makes your demo good — that's still the deck and the person delivering it. What it removes is the category of failure where the product is fine and the environment isn't. The AE stops opening a shared machine and hoping, and starts opening a machine created under a second ago from a snapshot that passed CI this morning. If the prospect trashes it in the first ten minutes, they trash it and get another one. Reset stops being a chore your team dreads and becomes something you can do live, on the call, as a feature.
Frequently asked questions
How long does it take to spin up a demo environment per prospect?
If you fork a pre-baked golden snapshot instead of provisioning infrastructure, a same-host fork on PandaStack lands in the 400–750ms range and a cross-host fork in 1.2–3.5s. That is fast enough for an AE to create a fresh environment during a call rather than filing a request the week before. It is fast because the application and its database were already running when the snapshot was taken, so there is no boot, no migration, and no seeding at request time — memory and the root filesystem are copy-on-write clones of the golden machine. Provisioning a staging copy through ordinary infrastructure automation is a different order of magnitude, because you are building a machine rather than copying one.
How do you reset a demo environment that a prospect has trashed?
Delete the microVM and fork the golden snapshot again. That is meaningfully different from running cleanup SQL, because a truncate-and-reseed script is a second copy of your schema that drifts the moment someone adds a table, while a re-fork restores the exact bytes of a machine that passed its bake tests. It also clears state cleanup scripts routinely miss: caches, search indexes, uploaded files, and background jobs mid-flight. The trade-offs are that the sandbox identifier changes, so front the environment with a stable hostname if links are shared in advance, and that anything the prospect built during the session is genuinely destroyed.
Do we really need a separate demo environment per prospect for data isolation?
It becomes necessary the moment prospects upload their own sample data, which is standard in any serious evaluation. On a shared demo instance that export sits in the same database every subsequent demo opens, and there is no guarantee the next viewer is not the uploader's competitor. A microVM per prospect gives each evaluation its own guest kernel, disk, and network namespace behind hardware virtualization, and lets the environment be deleted whole on a TTL — so the answer to a security questionnaire is about the boundary rather than about query discipline. It is an architectural answer, not a compliance program: it does not replace a data processing agreement, a retention policy, or a certification.
How do you stop POC environments from lingering for months?
Set a TTL when the environment is created, so it deletes itself on a known date instead of depending on someone remembering. Pair that with idle hibernation, which snapshots and stops an untouched environment so it costs storage rather than compute between sessions, and where waking it is a snapshot restore rather than a boot. Extension then becomes an explicit action tied to a conversation with the prospect about whether they are still evaluating, which is useful information for the deal as well as the bill. The combination means an environment used a few hours a week is priced like a machine that is off most of the time.
How do you keep the golden demo snapshot from going stale?
Treat it as a build artifact rather than a place. Keep the seed data as a script in the repository so it is reviewed like code, re-bake on every release tag and nightly in CI, and fail the bake if the specific pages your pitch depends on stop returning 200 — the bake is your smoke test. Generate seeded timestamps relative to bake time so dashboards showing recent activity are never empty, keep the previous golden snapshot as an instant rollback, and record which golden each demo was forked from in its metadata so you can reproduce exactly what someone saw.
Keep reading
- Snapshot and fork, explained — What a snapshot actually captures, and why a fork is copy-on-write rather than a copy.
- Seeding test data in ephemeral databases — How to make the golden database realistic, reviewable, and free of fixed timestamps.
- Preview environments on microVMs — The same fork-per-thing shape, pointed at pull requests instead of prospects.
- Scale-to-zero app hosting, explained — The idle-cost half: hibernate between calls and wake on the click.
49ms p50 cold start. Fork, snapshot, and scale to zero.