Docs

App updates

Once someone installs your desktop app, you will want to ship them fixes without asking them to download it again by hand. RubyEverywhere apps update themselves from an S3-compatible bucket you own: AWS S3, Cloudflare R2, Backblaze B2, MinIO, or anything else that speaks the S3 API.

This page covers doing it yourself with the every CLI. If you build on the hosted Platform, it can publish releases for you into the same kind of bucket, in the same format.

How it works

An update is two things in your bucket:

  1. The signed app artifact.
  2. An update manifest, a small JSON file naming the latest version, its URL, and its signature.

On launch, and on a timer after that, the installed app fetches {updates.url}/{channel}/{os}/{arch}/latest.json, compares versions, and if there is a newer one it downloads the artifact, checks the signature, and installs it. Because updates are signed with a key that never touches the bucket, a bucket compromise cannot push a bad build to your users.

Updates need code signing too. The updater installs a real macOS app bundle, so set signing up first.

1. Mint a signing key

The update signature is separate from your Apple certificate. Mint the keypair once per app:

Terminal bash
every updates keygen

The secret key lands in ~/.rubyeverywhere/keys/<bundle_id>.key, never in your repo, and the command prints the block to paste into everywhere.yml. It offers to encrypt the key with a passphrase; say yes unless a CI job needs to sign unattended.

Back the secret key up. Losing it strands every shipped app on its current version, because an app trusts exactly one public key and refuses an update signed with any other.

2. Configure the feed

Add an updates: block with the public base URL the app should check (your bucket's public endpoint, an r2.dev domain, a CloudFront distribution, or a custom domain in front of one) and the public key from step 1:

config/everywhere.yml yaml
app:
  name: Notes
  bundle_id: com.example.notes
  mode: local

updates:
  url: https://updates.example.com/notes
  public_key: "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3"
  channel: stable      # the channel this build polls
  auto: check          # off | check (notify) | download | install (silent)
  interval: 21600      # seconds between background checks, minimum 300

  s3:                  # publish-side only; never ships in the app
    bucket: my-app-releases
    endpoint: https://<account>.r2.cloudflarestorage.com
    region: auto
    # prefix: notes            # optional key prefix inside the bucket
    # force_path_style: true   # for MinIO and similar

Both url and public_key must be set or the updater is left out of the build entirely. url must be https, apart from loopback for local testing: the shell refuses to fetch an http feed and stops checking, so the app would ship looking fine and never update.

All of this is baked into the build, so set it before the version you want people to update from.

Omit endpoint for AWS S3 and set a real region instead.

3. Give the CLI write access

Publishing needs write credentials for the bucket. every publish reads the standard AWS variables, so keep them out of your repo:

Terminal bash
export AWS_ACCESS_KEY_ID="โ€ฆ"
export AWS_SECRET_ACCESS_KEY="โ€ฆ"
# export AWS_SESSION_TOKEN="โ€ฆ"    # only for temporary credentials

If your signing key has a passphrase, add EVERYWHERE_SIGNING_KEY_PASSPHRASE. In CI, where there is no ~/.rubyeverywhere, point at the key with --key, EVERYWHERE_SIGNING_KEY_FILE (a path), or EVERYWHERE_SIGNING_KEY (the key itself).

4. Publish a release

Cut a signed, notarized release, then publish it. every release writes dist/release.json, and that receipt is exactly what every publish reads:

Terminal bash
every release
every publish
Output text
โ†’ Signing Notes-1.4.0.zip
โœ“ signed and self-verified against updates.public_key
โ†’ Publishing 1.4.0 to stable/macos/arm64
โ†’ uploading Notes-1.4.0.zip (24.7MB)
  Notes-latest.zip alias refreshed
โœ“ latest.json โ†’ 1.4.0
โ†’ Published ๐ŸŽ‰
โœ“ Notes 1.4.0 is live on the stable channel
  feed https://updates.example.com/notes/stable/macos/arm64/latest.json
  4 versions in the index ยท shipped apps pick this up within 360 minutes

Bump app.version for each release. Publishing the same version twice with different bytes is refused unless you pass --force.

Add release notes with --notes "โ€ฆ" or --notes-file CHANGELOG.md. They reach users as plain text in the shell's update dialog and as HTML for an in-app changelog.

Bucket layout

You never manage these by hand, but for reference every publish writes:

Bucket text
my-app-releases/
  stable/macos/arm64/
    latest.json            # the manifest the app polls
    index.json             # every version published to this channel and target
    Notes-1.4.0.zip        # the artifact
    Notes-1.4.0.zip.minisig
    Notes-1.3.0.zip        # older releases, kept for rollback
    Notes-latest.zip       # a stable download link, refreshed each release

Each channel and target gets its own directory, so channels are parallel manifests in the same bucket. Add updates.s3.prefix to nest all of it under one key prefix.

Artifacts are immutable and cached forever; latest.json, index.json and the alias are marked no-cache. Keep the bucket, or its public URL, readable by anyone: the artifacts are signed, so serving them publicly is safe.

Rolling back

If a release turns out to be bad, point the manifest back at a version already in the bucket:

Terminal bash
every publish --rollback-to 1.3.0

Nothing is deleted, and the artifact is already there, so this is fast. Apps that already took 1.4.0 will not downgrade, so ship a higher version to move everyone off the bad build.

Update channels

Feeds are split by channel: stable by default, plus any others you publish to, like beta or nightly. Set the build's default with updates.channel, and publish elsewhere with every publish --channel beta.

Users can switch channels at runtime through the bridge, so you need no separate beta build:

app/javascript/controllers/app_settings_controller.js js
Everywhere.updates.channel                   // what the shell checks now
await Everywhere.updates.setChannel("beta")  // persisted across relaunches
await Everywhere.updates.check()             // scan the new channel right away

The shell saves the choice in the app's data directory, where it overrides everywhere.yml from then on. Because your settings page is server-rendered, you decide who even sees a channel picker: render the beta option only for users in your beta group and the shell honours whatever they pick. Switching back to stable never downgrades; the app stays where it is until stable moves past it.

Rails ยท Hanami ยท Sinatra โ€” built with Ruby