Deploying And Rollbacks
What It Is
How a Custom App release reaches users, and how to take it back. The same process runs whether you deploy from the app's page in the tenant, from the jetstack CLI or from CI, and every deploy and rollback is recorded in the app's Deployments history.
Why It Matters
A deploy can change more than code: a manifest v2 release also brings the data types, roles, grants and schedules it declares. Knowing what a deploy changes, and what a rollback does and does not restore, is what makes releasing safe.
Releases
- Uploading a bundle creates a release. Once published, it never changes.
- Releases are numbered v1, v2, … in the order they are published, per app. A draft has no number until it is published, and deleting a release or a draft never renumbers the others.
- A release is identified by a digest of its content. Uploading identical content again returns the existing release, which is how
promoteproves it deploys exactly what was tested. - An invalid v2 manifest is refused at upload with every problem listed. The rejected release stays in the list with its errors, and never runs.
What A Deploy Does
-
Plan. The platform compares what the release declares with the tenant, and shows it before anything changes:
create— missing; the deploy creates it.update— the app already owns it; the declared settings are applied again.adopt— it exists but was built in the builders; the app owns it from now on.rename— a property declaresrenamedFrom; it is renamed and keeps its data.remove— the app owned it and no longer declares it.
A deploy is refused when the plan names configuration another app owns, or references roles, types, secrets or environment values the tenant does not have.
-
Apply. Declared configuration is created and updated. Only one deploy of an app runs at a time; a second one gets a "try again" answer.
-
Migrate. The release's data migrations that have not run in this environment run now, if they are
blocking. The previous release keeps serving meanwhile. When they take longer than one request, the deploy shows asmigratinguntil they finish, and can be cancelled. A failed migration fails the deploy, with the previous release still live. -
Activate. The app switches to the new release at once. If anything failed before this point, the previous release is still serving and nothing is half-activated; deploying again continues where it stopped. The release's
backgroundmigrations start now, from the scheduler. -
Contract. Types, properties and roles the release no longer declares are removed only when you allow removals (
--allow-destructive, or the checkbox on the plan page), because removing them deletes their data. Until then they stay in place, and the plan keeps listing them. While the release's background migrations are unfinished, removals wait even when allowed. Grants and schedules the release no longer declares are always switched off and removed; that loses no data.
A v1 release (a manifest with routes only) deploys by activation alone.
A release with a managed runtime waits before it applies: the deploy is building while its service is built, then starting until the service answers, and only then applies and activates. The previous release serves meanwhile; if the build or the start fails, the deploy fails and nothing changes.
Evolving a schema safely
Because removal is a separate, explicit step, a schema can change without downtime:
- Add the new property and deploy code that writes both the old and the new one.
- Move existing data with a data migration in the same release.
- Deploy code that uses only the new property.
- Remove the old property, deploying with removals allowed.
Rollbacks
A rollback deploys an earlier release again — by default the one that was live before the current one:
- Code switches back immediately.
- Configuration the earlier release declares is applied again. Anything added since stays in place, and so does its data, unless you allow removals.
- A managed service starts again from the earlier release's image, which is kept, so nothing is built.
A rollback is recorded as its own deployment. It is not a database restore: data changed by the newer release stays changed, and so does data its migrations changed. The rollback lists those migrations. It never runs migrations itself: a release with migrations that have not run in the environment cannot be rolled back to, only deployed. For recovering the whole configuration of a tenant, see Version Management.
Approvals
An app can require a second person to approve each deploy. Switch it on with Require approval under Access on the app's page. Anyone who may deploy the app can switch it on or off, and each switch is recorded in the Event Log.
While approval is required:
- Requesting. A deploy from the app's page, the CLI or CI is planned and recorded as waiting for approval, together with the plan it showed. Nothing in the tenant changes yet.
- Approving. Another person with the
deployAppspermission approves it, either with Review and approve under Deployments on the app's page or withjetstack approve. The review shows the plan and, for the data migrations it would run, their code (jetstack approve --show-code). The person who requested a deploy cannot approve it. - Tokens can request a deploy but can neither approve nor reject one: a pipeline asks, a person decides. This holds for deploy tokens, promotion tokens and tenant tokens alike.
- Re-checking. Approving plans the release again. If the plan differs from the one the request showed, the deploy is refused and marked rejected, and has to be requested again. This happens when the tenant changed or another deploy ran in between, so nobody approves something other than what they saw.
- Turning down. Another deployer can turn a request down with a reason, and the person who requested it can withdraw it (
jetstack reject). - Rollbacks do not wait. They restore a release that was live before, and during an incident speed matters.
Each deployment records who requested it, who approved or turned it down and when, and the reason for a rejection.
Data migrations on production are explicit even for an app that does not require approval. What they need is set on production's History screen (productionMigrationApproval):
- by default, the person deploying confirms them: a checkbox on the plan page, or a question of its own in the CLI (
--allow-migrationsin scripts); - with a second person's approval, such a deploy waits as described above. The approval covers the migrations' code: a changed migration voids it.
Only migrations production has not let run yet count. One an earlier deploy let run, such as a background migration still going, is not asked about again, and its finishing does not void an approval waiting meanwhile.
Environments And Promotion
Test a release in one tenant, then deploy the same release to the next one:
jetstack promotecopies the releases that are live in one environment to another, unchanged, together with the configuration (see Promotion And Packages). It refuses if a content digest changes in transit.- The copy records where it came from: the tenant, the environment, the release number and the digest. The app's page shows the source next to the release (for example "← staging #12"), and the API returns it as
promotedFrom. - Uploading content the tenant already has returns the existing release, which keeps the origin recorded when it was first uploaded.
jetstack envsshows the live release in every environment of the project. Environments with the same digest run the same code.
An environment that requires approval still applies it: the promoted deploy waits like any other.
Editing The App: The App's Draft
Published releases never change, so editing an app works on a draft, a copy of a release. Each app has one draft, and everyone who works on the app changes it:
- its page in the tenant;
- the Studio;
- AI agents in the tenant and over MCP;
jetstack draftfrom a computer.
When it starts.
- The draft starts with the first change: an edited or new file, a folder, an upload, a rename, a move or a deletion.
- Until then, the draft's files are those of the live release, or else of the newest published one.
- Opening a release's files copies nothing. A change that fails or leaves the files as they were starts no draft.
- Opening the release the draft started from takes you to the draft.
Other releases. While the draft started from another release, other releases' files are only shown. Start the draft over from this version replaces the draft's files with that release's.
Nobody overwrites what they have not seen.
- Every change names the version of the file it replaces, so a change made meanwhile by someone else is reported instead of lost.
- Changes made on the app's page are the exception: they are saved as they are.
After publishing. Publishing turns the draft into a release, and the next change starts a new draft from it.
Earlier drafts. Drafts that belong to one person stay as personal drafts: those from before the app had one draft, and those of jetstack dev before version 0.17 (it now works on the app's draft). Copy into the app's draft continues one in the app's draft.
- Unused ones go. A personal draft that nobody used for 30 days (no file change, no line in its log) is removed by the daily maintenance, with an entry in the Event Log. So is one whose person is gone.
- The app's page shows when each one goes unless it is used, and Discard personal drafts removes them all at once. The app's draft is never touched.
The Draft's Data Model
A draft whose manifest adds or changes types, properties, enumeration items, roles, queries or grants declares a model that exists only after its release is deployed. Its routes cannot use it before that. On a development environment the model can be put there earlier, to try the draft:
- Where. In the Studio (Apply to development, offered while the draft's model is not there), with
appDraft.applyModel, or by an assistant withapp_model_apply. A check first says what would change. - What it applies. What the deploy would add or update in the app's own model.
- What waits for the deploy:
- renames, because live code still uses the old name;
- entities made outside the app, which the deploy takes over;
- removals;
- the app's principal, access, schedules, menu items and record actions.
- It becomes the app's. The history and promotions leave it out, as they leave out what apps own. The release's deploy then updates it.
- Discarding the draft, or starting it over, takes back its grants and queries at once. Types, properties and roles that may hold records stay. The Studio lists them and removes them with their records when asked (
appDraft.removeModel). Otherwise the next deploy removes them, when it is allowed to. - Production is not changed this way: there the model changes with a deploy.
On the draft:
- Preview serves the draft only to you, with a "Preview" bar and an exit link. It works for published but inactive releases too, and while the app is switched off or open only to roles you do not have. Your browser never keeps a preview's files in place of the live ones.
- It is sandboxed. The preview opens at an address of its own,
/app/<slug>/~preview/<token>/, as a page of its own origin, without your session. Its code may call the app's routes as you and make the SDK's data calls (records, uploads, jobs, search) as you, and nothing else: no administration, no changes to the draft, no other site. So code an AI agent wrote cannot act with your full rights. - Only you. The address opens only in your browser while you are signed in, ends after eight hours, and ends when you sign out. A link passed on opens nothing. Inside the Studio's preview, the app's own links between its pages and its reloads keep working, while no one else is signed in in that browser.
- Storage and cookies of the preview are kept in memory, until it reloads.
- Other sites. The preview loads scripts, images, fonts and data from the tenant only, unless the manifest's
preview.allownames other sites. What it blocks shows among the Studio's problems.
- It is sandboxed. The preview opens at an address of its own,
- Publish as new version turns the draft into a release: validated, identified by its digest, and ready to deploy.
- Discard draft throws the changes away.
Quick fixes stay possible, and every change still goes live through a deploy you can roll back.
jetstack dev works on the app's draft: the handlers and the manifest on your computer go there as you save them, each naming the version it replaces, and changes made in the Studio come back to your folder.
Branches
A branch is a named copy of the app's draft — "header-redesign", "agent-invoice-table" — changed on its own and merged back when it is ready.
- Why. Work that takes a while, or that you want to look at before it joins everyone's draft, stays apart: a redesign, an experiment, or what an AI agent builds while you review. The app's draft stays what everyone else works on and publishes.
- What a branch has of its own: its files, its revision, its preview, its builds and problems, and who has it open.
- It starts from the app's draft as it is then (before the draft's first change, from the release it would start from). That state is kept as the branch's starting point.
- Publishing is the app's draft's: a branch is merged first. So is the draft's data model.
- Merging brings the branch's changes into the app's draft as one change, and the branch goes. Each file is merged three ways — as the branch started, as the app's draft has it now, as the branch has it:
- a file only one side changed, added or deleted takes that side's version;
- lines both sides changed merge when the changes are apart;
- changes that overlap or touch, a file deleted on one side and changed on the other, and binary files changed on both sides are conflicts. You choose a side, or write the merged lines, before anything is written;
public/is left out: builds make it again.
- Taking the app's draft's changes into a branch works the same way the other way round. The branch then starts from the app's draft as it is now, so its changes are again only its own.
- Discarding a branch throws it away with its files.
A merge looks at both drafts as they were when you reviewed it: if either changed since, it asks you to look again. Branches are worked on in the Studio, with jetstack draft and jetstack dev (--branch), by AI agents (app_branch_*), and through the REST API.
Deploy Tokens
CI and the CLI authenticate with deploy tokens, created on the app's page under Deploy tokens:
- A token belongs to a dedicated, non-interactive App deployer user, not to a person, so it keeps working when people change roles or require multi-factor sign-in.
- A token can only upload, plan, deploy and roll back — one app, or all apps — and read their releases, deployments and logs. It can request a deploy that needs approval, but never approve or reject one. The tenant refuses it for everything else: records, MCP, webhooks.
- It is shown once, when created, and expires after the number of days you choose.
App Logs
Each app has a log that deployers read with jetstack logs or through the API:
- Handler errors, with the error id the caller received in the
500response. - Lines handlers write with
$jetstack->utils->log(). - Output a handler printed instead of returning, which the platform discards so it cannot corrupt a response.
Entries are kept for 14 days.
Best Practices
- Read the plan before every production deploy, and treat allowing removals as a data-deletion decision.
- Deploy to a non-production tenant first and
promotethe same release; never rebuild for production. - Require approval on production apps, and let pipelines request deploys rather than decide them.
- Keep a separate deploy token per pipeline and environment, scoped to the app, with an expiry.
- Roll back first and investigate afterwards: it is fast, and data is not lost.