Preview deployments
Every git branch gets its own deployment at a live URL, isolated from production — compute, Postgres database, environment variables and files.
A preview deployment is your app built from one git branch, running at its own URL with its own Postgres database. Push a branch that is not your default branch and Camplax deploys it there:
https://<branch>--<slug>.camplax.app
feature/login-redesign on the project my-app becomes https://feature-login-redesign--my-app.camplax.app. The branch part is cleaned up to be a legal hostname — lowercased, punctuation turned to dashes — and a very long branch name is shortened with a small hash so two similar names cannot collide. The URL is in the deploy's output (camplax deploy --json prints it), on the Deploys page, and on the pull request.
What a preview is
A preview is not a copy of the production site pointing at production data. It is a second, complete deployment of your app:
- Its own compute. The preview runs on its own instance, separate from the production app. It gets the same memory limit as your live app, so a preview of an app that needs 2 GB does not run out at 1 GB, and it does not take memory from your plan's pool. After 15 minutes with no visitors it goes to sleep, and the next visitor wakes it — your plan sets how many are awake at once (Indie 3, Pro 10, Scale all; when one more wakes, the one idle longest goes to sleep) and how many hours they are awake each month (below): visitors see a short "Waking up this preview" page that reloads by itself once the app is ready, usually within a minute. Its database stays up while it sleeps, so nothing is lost. Camplax's own uptime checks, cron calls and screenshots never keep a preview awake, and they do not wake a sleeping one. Production never sleeps.
- Its own environment variables. A preview reads the preview layer, then any values you set for that specific branch — never production's — so
STRIPE_KEYcan be a test key on previews while production keeps the live one. It also gets its ownBETTER_AUTH_SECRET, the key that signs sign-in sessions, so a preview session never works on production. The merge order is in Environment variables. - Its own Postgres database. A preview runs against a database created for that git branch — never the production tables. Your account's previews share one small Postgres server that is always on, each with its own database and its own login, so waking a preview only starts the app. More on this below.
- Its own files. Uploads on a preview go to a separate prefix in object storage, so a test upload cannot appear in the live app.
Because none of it is shared, a broken migration or a half-finished feature on a branch cannot touch the real thing.
Preview hours
Each plan includes hours of preview time a month — 100 on Indie, 300 on Pro, 1,000 on Scale — counted while a preview is awake, all previews together: two previews awake for an hour use two hours. A sleeping preview uses none, and a deploy to a sleeping preview wakes it, so it counts from then. Camplax checks which previews are awake every 15 minutes, so a single wake can count up to 15 minutes more or less than it ran; over a month these even out. The Usage page shows the hours used against your plan's.
Past the included hours, awake time is paid from your account's prepaid credits at 0.7¢ per GB-hour of the preview's memory limit (the same price as plan memory, $5 per GB a month), bought 100 GB-hours ($0.70) at a time before it is used. A preview of a 2 GB app costs 1.4¢ for each hour it is awake. You get an email the first time in a month that this starts.
When the credits run out, your previews are paused and you get an email: they go to sleep and stay asleep until you add credits or the month ends. Opening one shows a page that says it is paused, and no visit, uptime check or webhook wakes it. A new deploy to a paused preview still runs it for about 5 minutes so the deploy can finish, then it goes back to sleep. Production apps are never paused. The Usage page says when this is on. Add credits on the Billing page and previews wake on the next visit within a few minutes; otherwise on the 1st. Hours start again on the 1st of each month (UTC).
The database underneath
A preview gets its database branch automatically the first time its git branch is deployed — a push or pull request on a connected repo, or camplax deploy --branch <name> — and the deploy waits for it. It never falls back to production, and there is nothing to create by hand.
A branch database starts empty by default, and your checked-in migrations build it when the app boots. When the pull request closes — merged or not — the preview's compute, DNS and database branch are torn down together, so the whole thing is disposable.
Pull requests from a fork are never built. A fork's code is anyone's code, and a preview runs with your project's preview token and secrets, so only pull requests whose branch lives in the connected repository get a preview. To preview a contributor's change, push their branch to your repository.
Start from production's schema
Add a preview section to camplax.json:
{
"name": "my-app",
"preview": { "database": "schema", "seed": "db/seed.sql" }
}
With "database": "schema", every new preview starts with production's tables, types, functions and indexes, plus its migration history — and no rows and no sign-in users. Your app's boot migrations then run only the files this branch added. The deploy waits while the copy is made; if it fails, that deploy fails with the reason in its log, and the next push tries again. For a preview that already exists, Recreate with production schema on the Database page's branches tab does the same (POST /v1/projects/:slug/database/environments/<env-id>/recreate with { "withSchema": true }).
Camplax checks the copy itself before it lands: a schema copy that carries a single row is refused, whatever produced it.
Seed a preview
"seed": "db/seed.sql" (a path relative to your project root) runs once per preview, right after its first deploy goes live — so after your migrations. It runs in one transaction on the preview's own database, never production's. The outcome is written into that deploy's log and shown on the preview's row on the branches tab; a failed seed is rolled back whole, and the preview keeps serving. A preview that has already seeded never seeds again — fix the file, push it, then recreate the preview to run it again. A preview made with production's schema and no seed file starts with no rows; the branches tab says so and copies the snippet above for you.
Seed files are plain SQL: up to 48 statements and 256 KB (use multi-row INSERTs). psql commands such as \copy and COPY … FROM stdin are not SQL and are refused by name. Both settings are read from camplax.json at the commit being deployed, so they need a connected GitHub repository.
Or start from production's data
If you want to test against something real, deploy the branch, then use Recreate with production data on the Database page's branches tab (in the preview's menu). It tears the preview's database down and re-provisions it as a copy of production. It asks you to type the preview's name and to confirm it is you: your password, or a sign-in in the last ten minutes for an account without one, plus your two-factor code if you use one.
Only the project's Owner or a Member can copy production data, and only from a signed-in browser — never a Contributor, a project API key or a CLI token (camplax deploy --with-data now points you here). Restoring production as a new copy follows the same rule. Every copy is written to the project's audit log. Through the API it is POST /v1/projects/:slug/database/environments/<env-id>/recreate with { "withData": true, "confirmation": "<branch>", "password": "…" } from a browser session; POST /v1/projects/:slug/deploys with "withData": true takes the same confirmation and password, and only for a preview whose database does not exist yet.
The copy is made from production's continuous backup, so it carries whatever was there moments ago — raw, including rows, tokens and personal data; nothing is sanitized. Ask for it only on previews that should have it, and remember anyone with the preview link can reach it. To inspect the copy directly, pick the environment in the selector at the top of the Database page.
Locking previews behind a password
Previews are reachable by anyone who has the link. If you would rather they were not, open Settings and switch on Password-protect previews. Enabling generates a password, shown once — copy it and hand it to whoever reviews previews.
How it works:
- Every preview URL for the project shows a lock page before any of your code runs. A correct password sets an HttpOnly
cx_previewcookie that lasts seven days, per preview host. - Rotating the password invalidates everyone's cookies — the cookie's signature is keyed by the password hash.
- Each preview gets its own lock key, made from the password for that preview alone, so branch code you have not reviewed can only open its own preview. A preview switched to its own key on its first deploy after Oct 26 2026; visitors typed the password once more then.
- Production is never locked, and health-check paths stay open so the platform can still tell the app is alive.
- The lock takes effect on the next preview deploy — it is part of the app's environment, not a switch on live traffic.
The gate is also on the API: GET/PUT /v1/projects/:slug/domains/preview-gate, where PUT accepts enabled, rotate and reveal.
Letting CI through
End-to-end tests should not need the password. Instead, CI asks Camplax for a bypass token for one preview and sends it in the x-camplax-preview-bypass header. A token works on exactly one preview URL, lasts at most an hour, and dies the moment the password is rotated. Nothing is stored in CI.
From GitHub Actions no key is needed at all — the job proves which repository it runs in with GitHub's own OIDC token, and Camplax checks it is the repository connected to your project:
permissions:
id-token: write
contents: read
steps:
- name: Get a preview bypass token
run: |
OIDC=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://camplax.dev" | jq -r .value)
BYPASS=$(curl -sS -X POST https://camplax.dev/v1/preview-bypass \
-H "Authorization: Bearer $OIDC" -H "Content-Type: application/json" \
-d "{\"project\":\"my-app\",\"branch\":\"$GITHUB_HEAD_REF\"}" | jq -r .token)
echo "::add-mask::$BYPASS"
echo "BYPASS=$BYPASS" >> "$GITHUB_ENV"
- run: curl -fsS -H "x-camplax-preview-bypass: $BYPASS" https://$GITHUB_HEAD_REF--my-app.camplax.app/
In Playwright, set it once with extraHTTPHeaders: { "x-camplax-preview-bypass": process.env.BYPASS }. If previews are not locked, the answer is { "locked": false, "token": null } and the header is simply ignored.
Anywhere else, a signed-in member (or an optional project key used from outside Camplax) can mint one with POST /v1/projects/:slug/domains/preview-gate/bypass and { "branch": "<branch>" }. Every token minted is recorded in the project's audit log — the host and expiry, never the token.
Analytics and previews
Preview traffic is excluded from your analytics by default — a setting under Analytics → settings (excludePreview, on out of the box) — so a day of clicking through a branch does not bend your production graphs. Preview events that do arrive are kept separately and capped at 10,000 per month, against production's 250,000.
Comparing a preview with production
On the Database page's branches tab, Compare with production shows what a preview changed in the schema — new tables, columns, changed types and defaults, indexes, foreign keys, check and unique constraints, enums, functions, triggers, row security and its policies — as a summary and as the SQL that would apply it. Both schemas are read; no rows are.
Merge into production applies exactly that SQL to production as one recorded migration: all of it or none of it. What runs is what you reviewed — if either schema changed in between, the merge is refused and you review again. Anything that deletes data (a dropped table or column, a type change that can fail) needs a second confirmation.
One case is refused outright: if the preview ran migration files from your branch that production has not, those files will run on production when the branch is deployed. Merging their effect first would make that deploy fail, so ship the branch instead — the review panel names the files. The reverse is refused too: if production ran migration files after the preview was made, the comparison would undo them — recreate the preview with production's schema, then compare again. Dropping an index needs the same second confirmation as deleting data.
On the API: POST /v1/projects/:slug/database/branches/<env-id>/diff, then POST …/merge with { "fingerprint": "<from the diff>", "confirmDestructive": true } when needed. Agents use the MCP database tool's diff_branch and merge_branch.
The SQL runs in an order where everything a statement needs already exists — an enum before the column that uses it, a function before its trigger, both tables before the foreign key between them. Removing a rule (a foreign key, a check, a trigger, a function), changing or dropping a row-security policy, or turning row security off needs the same second confirmation as deleting data. Adding a foreign key or check to an existing table checks every row; if one breaks it, the whole merge rolls back. Postgres cannot remove or reorder an enum value in place, so a diff with one is refused — change that enum by hand.
Shipping a preview to production
Merging the PR is the normal path — the merge lands on the default branch and deploys production. To ship the branch as production without merging, promote it:
POST /v1/projects/:slug/deploys/previews/<branch>/promote
Production then builds exactly what the branch's newest ready preview was built from — the same commit, or the same uploaded folder — with production's own settings, not whatever the branch holds by now. A branch with no preview answers 404 preview_not_found and one whose preview never finished answers 409 preview_not_ready; neither queues anything.
Good to know
- Preview URLs only exist on
*.camplax.app. Custom domains always reach production — you cannot point a custom domain at a branch. - The newest push wins: pushing again to the same branch cancels the preview still building and queues a fresh one.
- A preview is tied to the branch name, so a renamed branch is a new preview, not a moved one.
Under the hood: each preview is its own cell environment — a Kubernetes namespace, its own Postgres database, its own R2 prefix and its own secrets scope — suspended when idle, deleted with the branch. A data copy is the database's WAL archive replayed into the preview's cluster, so the data never crosses a network boundary twice.