Electron OTA Updates (Beta)
An Electron app usually ships every change through a new installer and an auto-updater. With Zephyr, you can move part of the renderer into a Module Federation remote, deploy that remote to Zephyr Cloud, and have the installed app pick up new releases from a Zephyr environment. Users get the update in seconds, without a download or a restart, and a rollback is just as fast.
This guide builds that flow with Vite. One feature of the app (a settings page, a dashboard, a usage view) becomes a remote. The app loads it from an environment URL, watches that URL for new releases, and offers an in-place update when one appears.
Electron OTA support is currently in beta. APIs and recommended wiring may change while the Electron integration continues to stabilize.
OTA updates replace JavaScript and CSS that the renderer loads through Module Federation. Changes to the main process, preload scripts, native modules, the Electron version, and the app shell still need a normal release through your installer or auto-updater.
How It Works
- The feature lives in its own Vite app that exposes a component through Module Federation.
- A release build sets
ZE_ENV=production. Zephyr uploads it as a new immutable version labeled with that environment. - A tag with an Environment condition matches the newest version labeled
production. Theproductionenvironment follows that tag, so every release build goes live automatically. - The Electron renderer loads
remoteEntry.jsfrom theproductionenvironment URL with the Module Federation runtime. - While the app runs, it polls the environment's
remoteEntry.js. When the file changes, a new release is live, and the app shows an update prompt. - Applying the update loads the new release and swaps the component in place. No reload, no restart.
Rolling back works the same way: when the environment serves an older version, the app sees a different entry and offers it as the next update.
Prerequisites
- An Electron app whose renderer is built with Vite
- A Zephyr account and the Prerequisites setup
- A feature you can render as a single React component
Create The Remote
Create a Vite app for the feature, next to the Electron app. Install the Module Federation and Zephyr plugins:
Expose the feature's component and share React as a singleton, so the remote renders with the app's React instead of its own copy:
Pin the Zephyr organization and project with a declarative configuration file, so release builds land in the same place no matter which Git remote the checkout uses:
Add a release script. ZE_ENV labels the build with the environment it is meant for. On its own, it doesn't deploy anything; the tag in the next step turns that label into a release.
Run release once now. Zephyr needs one version of the app before you can create its tag and environment.
Release Through A Tag
In the Zephyr dashboard, open the remote's application and set up two things:
- A tag named, for example,
env-production, with one condition: Environment isproduction. It resolves to the newest version built withZE_ENV=production. - An environment named
productionthat follows theenv-productiontag. See Environment Configuration for tag-based environments.
Copy the environment URL from the environment's page. It has the form https://production-{app-uid}-{hash}.{domain}, and the remote entry is at /remoteEntry.js under it.
From now on, pnpm release is the whole release process: the build uploads, the tag picks it up, and the environment serves it. Builds without ZE_ENV=production, such as local or preview builds, still upload as versions but never reach users.
To release only from CI, add a CI condition to the tag next to the Environment condition. Local builds labeled production then stay out of the environment. See Tag Conditions.
Load The Remote In The App
The Electron renderer is the Module Federation host. It shares React with the same singleton settings as the remote and registers the remote at runtime, because the URL it loads changes with every release:
Two small functions cover the Zephyr side. loadRelease loads whatever the environment serves now, and hasNewRelease tells you when that has changed:
Two details make the in-place update work:
- The query string. Browsers cache ES modules by URL, and the environment URL never changes. Without a new URL per load, the browser returns the module it already evaluated.
force: true. It replaces the registered remote and clears the runtime's cached module, soloadRemoteevaluates the new release instead of returning the old one.
Polling compares the text of remoteEntry.js. Each release lists different chunk hashes, so a new release always changes the file.
Render The Feature And Offer Updates
Keep the loaded component in your app's state, and render it where the feature used to be. Keep a copy of the feature bundled with the app as the fallback for when the remote can't load: the user is offline, the edge is unreachable, or a release throws while rendering.
resetKeys clears the error boundary when a new release is applied, so a fixed release replaces the fallback. The example uses react-error-boundary, but any error boundary that supports resetting works.
Call hasNewRelease() on an interval from the app shell, so the update can be offered on any screen, not only the federated one. Show it wherever your app already reports updates, such as a status bar item or the auto-updater's indicator. When the user applies it, call loadRelease() again and store the new component. If the federated screen is open, it re-renders with the new release. If it isn't, the user sees the new release the next time they open it. Nothing has to navigate or reload.
Allow The Remote In The Content Security Policy
Electron apps usually ship a strict Content Security Policy. Allow the origin of the environment URL you copied from the dashboard for scripts (the remote entry and its chunks) and for fetch (polling). Zephyr environment URLs end in zephyrcloud.app or zephyr-cloud.io, so use whichever domain yours ends in:
If the remote ships CSS, Vite loads it with a <link rel="stylesheet"> tag, so add the same origin to style-src. Add it to font-src and img-src too if the remote ships fonts or images.
Set it wherever your app defines its policy: a <meta http-equiv="Content-Security-Policy"> tag in index.html, the headers of a custom protocol handler, or session.webRequest.onHeadersReceived. If you serve Zephyr from a custom domain or your own cloud integration, allow that origin instead.
Ship And Roll Back
With the app installed and running:
- Change the feature and run
pnpm releasein the remote. - Within one poll interval, the update prompt appears in the app.
- Apply it. The feature re-renders with the new release, and app state outside it is untouched.
To roll back, deploy the previous version to the production environment from the dashboard (Instant Rollbacks), or run pnpm release again from the previous commit. Either way, the environment serves a different entry, and the app offers it as the next update.
Lock the environment after a rollback so a new release build can't replace it until you unlock it.
Troubleshooting
The update applies, but the old release keeps rendering. The runtime is reusing the module it already evaluated. Register the remote with a URL that changes per release (the release query string above) and pass force: true to registerRemotes.
The remote's styles break the app layout. If the remote ships its own utility CSS (for example, Tailwind), its rules can land after the host's and override them. A remote .hidden that comes after the host's md:block hides host elements at every width. Put the remote's utilities in a lower cascade layer than the host's, such as @import "tailwindcss/utilities" layer(components), or scope them to the remote's root element.
The app retries a broken release on every poll. hasNewRelease() keeps returning true after loadRelease() fails, because the loaded release hasn't changed. Remember the remoteEntry.js text of the release that failed, and offer the update again only when the environment serves a different one.
React hooks throw "Invalid hook call". The remote is running its own copy of React. Make sure both the host and the remote declare react and react-dom as singleton shared dependencies.
Nothing updates after a release build. Check that the build ran with ZE_ENV=production, that the new version appears under the env-production tag, and that the production environment follows that tag rather than a fixed version.