Browse documentation

One isolated stack per pull request: its own PostgreSQL, its own storage, its own Studio, on a subdomain of the wildcard record you already have. Destroyed when the pull request closes.

This is the cheapest feature on the roadmap because the hard part was already built. Provisioning an isolated project from a Compose file is what the Console does; this is the glue between a GitHub webhook and the orchestrator.

Setup

On the Console host:

PREVIEW_ENABLED=true
PREVIEW_GITHUB_SECRET=$(openssl rand -hex 32)
PREVIEW_REPO_ALLOWLIST=acme/app,acme/dashboard
PREVIEW_GITHUB_TOKEN=ghp_...     # optional, only to post the PR comment
PREVIEW_TTL_HOURS=72             # 0 disables the sweeper

Then in the repository, Settings → Webhooks → Add webhook:

FieldValue
Payload URLhttps://console.example.com/api/preview/github
Content typeapplication/json
Secretthe PREVIEW_GITHUB_SECRET value
EventsLet me select individual events → Pull requests

Previews stay off unless a secret is configured. A receiver with no secret verifies nothing, so PREVIEW_ENABLED=true on its own does nothing.

What happens

Pull request eventEffect
opened, reopened, ready for reviewProvisions pr-<number>-<repo> and comments the URLs on the PR
synchronize (a new push)Restarts the existing stack. It is not recreated: that would drop the database and any state a reviewer had set up
closed or mergedDestroys the stack and its volumes
draft openedIgnored until marked ready. A database per work-in-progress push is how this becomes a surprise bill

The project name is derived from the repository name and the pull request number, both immutable for the life of the PR. Deliberately not the branch name, which can be renamed mid-review and would orphan the first stack.

The allowlist is not optional

Warning

PREVIEW_REPO_ALLOWLIST empty means no repository may provision. The receiver fails closed on purpose: a webhook URL is not a secret — it sits in a repository setting several people can read — and an open receiver lets anyone who learns it spend this host's disk and CPU.

Authentication is the HMAC signature over the raw request bytes, verified in constant time. The raw bytes matter: re-serialising the parsed JSON does not reproduce what GitHub signed, so a naive implementation both rejects valid payloads and, worse, accepts some altered ones.

The webhook is answered immediately and provisioning continues in the background. A stack takes minutes to come up and the delivery would time out long before that, which would make GitHub retry and start a second one.

The sweeper

A teardown can be missed: a webhook delivery fails, a repository is deleted, a PR is closed while the Console is down. Every hour the Console destroys preview projects older than PREVIEW_TTL_HOURS.

Two rules it will not break:

  • Only preview projects. A name has to match pr-<digits>-<slug> to be a candidate. A real customer project is never swept, however old it is.
  • Never on a date it cannot read. A project with a missing or unparseable createdAt is left alone. Leaving a stack up costs disk; destroying someone's database on a bad date string has no equivalent undo.

Cost

A preview is a full stack: Postgres, MinIO and an API container. On the default "small" tier that is roughly 1.25 CPU and 1.3 GB of RAM per open pull request. Budget for your busiest day, keep PREVIEW_TTL_HOURS short, and keep the allowlist to the repositories that genuinely need it.