Required Intermediate Build
Overview
A required intermediate build is a version that every client must install before it is allowed to jump past it. When an install on an old version checks for updates, faynoSync offers the intermediate instead of the latest release. Once the client is on the intermediate, the next check offers the latest one.
You need this when a release cannot be skipped:
- the intermediate version migrates a local database, config file or cache format that later versions no longer know how to read;
- it rotates keys, certificates or an update feed URL that newer versions depend on;
- it ships a fix that has to run before a newer installer can work at all.
Without an enforced path, a user who has been offline for months jumps straight from 1.0.0 to 3.0.0 and skips the migration in 2.0.0. A required intermediate build makes that impossible: the decision happens on the server, so the client needs no special logic.
How the server picks the version
On every update check (/checkVersion, the SDKs, and the dynamic Velopack feed), faynoSync looks for versions of the same app that are:
- marked as intermediate (
intermediate: true); - published;
- in the same channel the client asked for;
- carrying an artifact for the client's platform and arch;
- newer than the client's version and not newer than the latest version.
If several versions match, the lowest one is offered. The response then describes that version entirely: its artifacts, its changelog, its critical flag and its rollout percentage, plus is_intermediate_required: true.
If nothing matches, the client gets the latest version as usual and is_intermediate_required is left out of the response.
Example: versions 1.0.0, 1.0.1 (intermediate), 1.0.2 and 1.0.3 (intermediate), 1.0.4 are published on stable.
| Client version | Offered version | is_intermediate_required |
|---|---|---|
1.0.0 | 1.0.1 | true |
1.0.1 | 1.0.3 | true |
1.0.2 | 1.0.3 | true |
1.0.3 | 1.0.4 | absent |
1.0.4 | no update | absent |
Each install moves one step per update, so a client that is several intermediates behind walks the chain in order.
Marking a version as intermediate
On upload
Add "intermediate": true to the data JSON of /upload:
curl -X POST --location 'http://localhost:9000/upload' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'file=@"/path_to_file/myapp-1.0.1.deb"' \
--form 'data="{\"app_name\":\"myapp\",\"version\":\"1.0.1\",\"channel\":\"stable\",\"publish\":true,\"intermediate\":true,\"platform\":\"linux\",\"arch\":\"amd64\",\"changelog\":\"Migrates the local database to the v2 schema\"}"'
With the faynoSync CLI, pass --intermediate:
faynosync upload \
--app myapp \
--file ./myapp-1.0.1.deb \
--version 1.0.1 \
--channel stable \
--platform linux \
--arch amd64 \
--publish \
--intermediate \
--changelog "Migrates the local database to the v2 schema"
For presigned uploads, intermediate is taken from the upload that creates the version, like publish, critical and rollout.
On an existing version
Set the flag later with /apps/update:
curl --location 'http://localhost:9000/apps/update' \
--header 'Authorization: Bearer <jwt_token>' \
--form 'data="{\"id\": \"653a6268f51dee6a99a3d88c\", \"app_name\": \"myapp\", \"version\": \"1.0.1\", \"channel\": \"stable\", \"publish\": true, \"intermediate\": true, \"platform\": \"linux\", \"arch\": \"amd64\", \"changelog\": \"\"}"'
/apps/update directlyThe dashboard always sends the current intermediate value, so editing a version there keeps the flag. When you call /apps/update yourself, include intermediate in data: a missing field is treated as false.
Response examples
Intermediate step required
A client on 0.0.1 checking for updates, while 0.0.5 is a required intermediate and 0.0.8 is the latest:
{
"changelog": "Example changelog\n",
"critical": true,
"is_intermediate_required": true,
"update_available": true,
"update_url_deb": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.5.deb",
"update_url_rpm": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.5.rpm"
}
No intermediate step
The same client after it has installed 0.0.5:
{
"changelog": "Example changelog\n",
"critical": false,
"update_available": true,
"update_url_deb": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.8.deb",
"update_url_rpm": "https://<bucket_name>.s3.amazonaws.com/secondapp/stable/linux/amd64/secondapp-0.0.8.rpm"
}
The JavaScript SDK exposes the flag as isIntermediateRequired, the Go SDK as IsIntermediateRequired. Use it in the UI, for example to explain that another update will follow right after this one.
Support by updater
| Updater | Intermediate builds | Notes |
|---|---|---|
Manual (/checkVersion, SDKs) | Yes | The response points at the intermediate's artifacts. |
| electron-builder | Yes | The yml package URL belongs to the intermediate's version folder, so electron-updater downloads that version. See the Electron example. |
| Squirrel macOS / Squirrel Windows | Yes, via /checkVersion | Same server decision, returned in the updater's response format. |
| Tauri | Yes, via /checkVersion | Same server decision. |
| Velopack | Dynamic feed only | Pass localVersion to the dynamic feed; faynoSync caps the feed at the intermediate. The static CDN feed cannot make per-client decisions. |
| Sparkle | Not yet | Needs a dynamic appcast route; the static appcast is the same for every client. |
Edge cache
With the edge and S3 response cache, manifests are stored per client version (.../{client_version}.json), so a client on 1.0.0 and a client on 1.0.2 read different cached answers and the intermediate decision stays correct. An upload or an /apps/update call clears the app's manifests for that channel, so a newly marked intermediate takes effect on the next miss (allow up to a minute for a CDN in front of the bucket).
Staged rollouts
An intermediate carries its own rollout percentage. The rollout of the final version only applies once the install is already on the intermediate, and chained rollouts multiply. See Interaction with required intermediate versions.
Use cases
- Breaking changes: a new version changes a file format or protocol, and only the intermediate knows how to convert the old one.
- Database migrations: local schema changes have to run in order.
- Security updates: a key rotation or a new trust anchor has to be installed before later releases are accepted.
- Feature dependencies: new features rely on data or settings created by the intermediate.
Best practices
- Record in the changelog why a version is a required step. The client shows the intermediate's changelog, not the latest one.
- Keep the chain short. Every intermediate is one more download and restart for users who are far behind.
- Make the intermediate stable and well tested. Users cannot skip it, so a broken intermediate blocks everyone behind it.
- Upload the intermediate for every platform and arch you ship. A client whose platform/arch has no artifact on the intermediate is not stepped through it and gets the latest version directly.
- Do not unpublish an intermediate while installs older than it still exist. An unpublished version is ignored, so those installs skip straight to the latest.
- Mark the version critical as well if it must be installed promptly;
intermediatecontrols the order, not the urgency.
Troubleshooting
The latest version is offered instead of the intermediate. Check that the intermediate is published, is in the same channel the client sends, has an artifact for the client's platform and arch, and is newer than the client's version. If you edit versions through the API, also check that the flag is still set: an /apps/update call without intermediate resets it to false.
Clients on the intermediate are not moving on. That is the expected behavior while the next version is under a staged rollout and the device's bucket is outside it. See Staged rollouts.
A Velopack client skips the intermediate. The client is reading the static CDN feed. Point it at the dynamic feed and send localVersion.
Limitations
- One step per check: only the lowest matching intermediate is offered; the client reaches the next one on its following update.
- Versions are compared numerically (
1.0.10is newer than1.0.9), so they have to be valid version strings. - Sparkle and the static Velopack feed do not support intermediate builds yet.
Related reading
- Staged Rollouts — how rollout percentages combine with intermediate steps.
- Velopack updater — intermediate stepping through the dynamic feed.
- Check Latest Version API — the
is_intermediate_requiredresponse field. - faynoSync CLI — the
--intermediateupload flag. - Rollout Health Reports — Catch Failed Updates Before They Spread — watch the intermediate's adoption before pushing the next step.
- How to Setup Auto Update for Electron App — the client side of the flow.