Preview deployments

Every pull request gets a real URL

Open a PR against an app's tracked branch and Sproobo builds that PR's head commit and serves it at <app>-pr-<n>.<your apex>, with valid TLS, on your own server. It rebuilds on every push and disappears when the PR closes. Reviewers click a link instead of pulling a branch.

What you need before turning it on

Previews are opt-in per app, and four things have to line up. The toggle tells you which one is missing:

  • A Pro plan or above. Previews consume off-box builder compute, so they're a paid-tier feature. Free orgs don't get them.
  • An active wildcard domain on the app. Every preview host is one label under a *.apex certificate you already hold, which is why a preview URL is valid the instant it exists, with no per-PR ACME wait. Without a linked, active wildcard domain there is nowhere to put the preview.
  • A connected GitHub App installation subscribed to the pull_request event, with Pull requests: read & write permission. The event is what fires the preview at all; the write permission is what lets Sproobo post the status comment.
  • The Previews toggle on the app's page, off by default.

A PR only previews when its base branch equals the app's tracked branch. A preview answers "what would production become if this merged," so it follows the same branch auto-deploy does. If the app has watch paths configured, they filter previews identically: a PR touching only apps/web/** previews only the web app.

The lifecycle

01

PR opened

Sproobo creates the preview, provisions its scratch databases, and posts a single comment on the PR: 🔨 sproobo preview — building…

02

Build and deploy

The PR's head commit goes through the same off-box build and health-gated blue-green pipeline as a production deploy. Same builder, same registry, same agent, same health gate.

03

Live

The comment is edited in place, never duplicated: ✅ sproobo preview — ready: https://<host>, with the deployed commit sha.

04

Push again

Every push to the PR redeploys the preview against the same host and the same scratch database. The old preview container is retired by the normal blue-green swap. Previews keep exactly one release; there is no preview rollback, you fix forward by pushing.

05

PR closed

Merged or not, closing tears everything down: the proxy route, the container, the scratch databases and their users, and the preview's registry tags. The comment becomes 🗑 sproobo preview — torn down.

If a deploy fails, the preview goes to failed and the comment links straight to that build's logs in the dashboard. Scratch databases are kept, so the next push retries against the same data.

Each preview gets its own database

This is the part that makes previews safe to hand to anyone. On a preview's first deploy, Sproobo creates a scratch database and a least-privilege user scoped to it, inside the app's existing service container. Not a copy of production. Not read-only access to production. A separate database, dropped on teardown.

The credentials are written over the same environment variable name the production binding uses, so your app needs no code change to be preview-safe: it reads DATABASE_URL exactly as it always did, and that URL points at the same service container but a different database and user.

  • Postgres, MySQL and MariaDB have database-level isolation, so they get a scratch database per preview.
  • Redis and Elasticsearch don't, so a preview shares the app's production instance for those. Redis is a cache and the pollution is short-lived, but it is a real difference: if you keep anything in Redis that a preview must not touch or evict, know that today it can.
A preview holds no privileges on your production tables.

The isolation isn't a convention the app has to honor. The preview's user is granted rights on its own scratch database and nothing else: it is not a superuser, it cannot create databases or roles, and it holds no table privileges outside its own database. Even a preview running deliberately hostile code from a branch cannot read or write a row of production data.

Worth being exact about where that boundary sits. Postgres grants CONNECT on every database to PUBLIC by default, so a preview's user can still open a connection to another database on the same service container and read catalog metadata: table and column names, never their contents. If schema names are themselves sensitive in your environment, revoke CONNECT from PUBLIC on those databases.

Running migrations: the init command

A PR that adds a migration needs that migration applied to its scratch database before the preview is routed. Set an app's optional preview init command and Sproobo runs it via sh -c inside the freshly built image, with the full preview environment, after the image lands on the box and before any traffic reaches it:

npx prisma migrate deploy
bundle exec rails db:migrate && rails db:seed

It runs on every preview deploy, which is exactly what applies a PR's new migrations on each push, so it must be idempotent. Migration tools already are; a seed script has to guard itself. A non-zero exit fails the deploy, the routed state never changes, and production is untouched. Leave it unset and your app is expected to migrate on boot.

What the preview knows about itself

A preview gets the app's normal environment, plus three variables:

  • SPROOBO_PREVIEW=1, the flag to branch on for anything a preview should do differently: disable analytics, point at a payment provider's sandbox, show a banner.
  • SPROOBO_PR_NUMBER, the pull request number.
  • SPROOBO_PREVIEW_URL, the preview's own https:// URL, for anything that needs an absolute callback or OAuth redirect.

If the app has access protection enabled, the preview inherits it, so a password-gated app stays password-gated in review.

Limits and cleanup

  • Three concurrent previews per app. A fourth open PR gets a comment saying the limit is reached, and no deploy. Close another PR to free a slot; a later push re-checks.
  • Seven-day idle TTL. A preview with no push for seven days is torn down automatically. Push to the PR and it comes back.
  • Destroy on demand. Tear a preview down from the dashboard at any time without touching the PR.
Pull requests from forks never build. By design.

A build receives your app's environment, so building a fork's code would hand your secrets to whoever opened the PR. Sproobo refuses fork PRs outright, with no trusted-fork escape hatch. If you take contributions from forks, review the diff and merge it to a branch in your own repo to get a preview of it.

Next