A preview is a throwaway copy of a project at its own addresses: to try a pull request, or any change, without touching the real thing.
A preview's databases start empty. Give the project a seed command in alf.toml:
[previews]
seed = "npm run db:seed"
Every new preview runs it once, after its app is first live (and its migrations have run): the app's own seed script, with made-up data rather than anybody's real data. alf previews shows each preview's seed: waiting for its app, running, done, or failed with its exit code (a failure is also sent where deploy failures go).
In a project, + Create → Preview environment. Optionally give it a name (like pr-12), and choose how long to keep it: 6 hours to a week. Coding agents can create previews too.
Then push code to it like to any project: its services have their own Deploy new code, and the deploy command takes --preview <name>.
In CI, --preview <name> does both steps: it deploys to the project's preview of that name, and creates the preview the first time. --end-preview <name> ends it when the pull request closes. The workflow for GitHub Actions is ready to copy, and alf ci github writes it for you, with a step that comments the preview's addresses on the pull request: one comment, updated with each push, and saying when the preview ended. It uses the job's own token, so there's nothing to install on GitHub. (deploy.sh hands the addresses to the steps after it as the output urls, one "service address" per line, for a workflow of your own.)
Anyone in the organization can end a preview. When its time is up, it's deleted with its services and their data.
A preview is a copy that expires, because CI makes one per pull request and nobody would clear them up. An environment is the same copy without the expiry: a staging you deploy to first, beside the production everyone uses. Nothing deletes it.
Make one from a project's Environments page, in its sidebar. It gets the same services, settings, variables and secrets, its own addresses, and volumes that start empty. Custom domains stay with the original — point your own at the environment if you want one.
A copy — preview or environment — has everything the original has, its own of each:
${{postgres.DATABASE_URL}} and ${{uploads.S3_BUCKET}} find the copy's database and bucket, never the original's. Production's data stays in production.To give a copy some data, restore a snapshot you downloaded, or seed it the way you would a new database.
Record what's running writes down the version every service in an environment is on, under a name. Open a release to see exactly what it holds, and Promote puts those same versions into another environment. Forget it throws the record away; whatever was promoted from it carries on running.
Only the versions move. The other environment's variables, secrets, domains and volumes are its own and aren't touched — that's the point, so production keeps pointing at the production database. A promotion is refused unless every service in the release exists there, so it can't half happen and leave a mixture running.
A service built from pushed code is built again from the same commit rather than running the image that was tried, because an image belongs to the service that built it. That's the same source, not the same binary; the answer says which services were rebuilt. If that matters, build in CI and deploy a registry image instead.