Sparkle is the updater. Railcast is the backend it needs.
Sparkle handles the client side of auto-updates on macOS — checking for new versions, downloading, verifying, installing. It deliberately doesn't host anything: the appcast feed, the release files, the signing — that part is on you. In practice that turns into a Supabase Edge Function nobody wants to maintain, an appcast.xml hand-edited on a raw GitHub URL, or a full backend built to serve one XML file.
Railcast is that missing piece. Push a build, get back a signed, hosted feed. Nothing to run, nothing to keep alive.
- A hosted
appcast.xmlfor each app, served fast and cached at the edge - EdDSA signing built in — every release is verified before Sparkle installs it
- Release channels (stable / beta) out of the box
- A CLI that turns "build → signed, hosted release" into one command
Solo and small-team macOS developers shipping a native app who want Sparkle's update experience without owning the infrastructure behind it. If you've ever thought "I just need somewhere to put this XML file," this is for you.
- Create an account — railcast.casablanque.com/register, email + password. Confirm the email that gets sent; that link also logs you in.
- Get a token — on the dashboard, under "Get a token", click Generate new token. It's shown once and copied to your clipboard automatically — save it somewhere, it can't be viewed again (you can always revoke it and generate a new one).
- Install the CLI:
curl -fsSL railcast.casablanque.com/install.sh | sh - Set your token for the session (optional, but every command below assumes it):
export RAILCAST_TOKEN=<token from step 2>
From the directory where your build lives:
railcast init --app testappThis generates an Ed25519 signing key, registers a new app on the server, and saves two things in the current directory:
testapp.key— your private signing key (mode0600). Never share it, never commit it. Losing it means you can no longer publish updates for this app — there's no recovery..railcast.json— the app's real (server-generated) id and the path to the key.railcast publishreads this automatically from now on, so you don't need to pass--app/--keyagain as long as you runpublishfrom this same directory.
init also prints an SUFeedURL and SUPublicEDKey — add both to your app's Info.plist once, so Sparkle knows where to check for updates and which key to trust.
Then publish the first build:
railcast publish --version 1.0.0 --file testapp-1.0.0.zip--build is optional — leave it out and Railcast assigns the next build number for that channel itself, printed after publishing (build: 1). You never have to track or remember it.
From the same directory (so .railcast.json is picked up):
railcast publish --version 2.0.0 --file testapp-2.0.0.zip- The archive file must have a name Railcast hasn't seen before for this app (see Gotchas below) — the filename itself has to change every release, bumping
--versionalone does not satisfy this. Baking the version into the filename (as above) is the simplest way to guarantee that. - If you do pass
--buildexplicitly, it must be strictly greater than the previous build on that channel — the server rejects anything else. There's normally no reason to pass it; it exists for cases like mirroring build numbers from an external CI system.
Optional flags for either a first publish or an update:
| Flag | Purpose |
|---|---|
--build <n> |
Explicit build number, must be greater than the channel's current latest. Normally omitted — see above. |
--channel beta |
Publishes to a separate channel instead of stable. Build-number ordering is tracked per channel, independently. |
--notes "…" |
Plain text or Markdown release notes, shown in Sparkle's update dialog. |
--notes-file path |
Same, read from a file — overrides --notes if both are given. |
--critical |
Marks the update as critical (sparkle:criticalUpdate) — Sparkle won't let the user postpone it. |
--phased-rollout <seconds> |
Staggers the rollout to installed clients (sparkle:phasedRolloutInterval). 0 (default) disables it. |
Run railcast publish --help any time for the full, current flag list.
- Upload filenames are permanent per app. Once
appid/filenamehas a published version attached, that exact filename can never be re-uploaded for that app — it's intentional (nothing should be able to silently swap the bytes behind an already-signed, already-published release). If you get"...zip" was already published for this app, the fix is to rename the archive, not to change--version/--build. Baking the version into the filename up front avoids ever hitting this. --buildis optional, and per channel, not global. Leave it out and Railcast auto-assigns the next one for that channel (starting at1);stableandbetaeach track their own count independently, sobetasitting on build5doesn't conflict withstablealready being on12. Pass--buildexplicitly only if you have a specific reason to (e.g. mirroring a build number from external CI) — an explicit value still has to be strictly greater than that channel's current latest, or the publish is rejected.- The signing key never touches the server.
initgenerates it locally and only ever uploads the public half. If<app>.keyis lost, there is no way to publish further updates to that app under the sameSUPublicEDKey— you'd need a new app (new key, new feed URL, and existing installs would need to be pointed at it some other way, which Railcast doesn't automate). --appatinittime is just a local label — it picks the default key filename and shows up in your terminal, but the id Railcast actually uses (in the feed URL, in--appforpublish) is a separate, server-generated id written into.railcast.json. You don't need it to be unique across all Railcast users.- Publishing from a different machine or directory (no local
.railcast.json/key) means passing--app <id>and--key <path>explicitly topublish— copy both from whereverinitoriginally ran. Don't runinitagain for an app you already have; that creates a brand-new app with a brand-new key, not a continuation of the old one. - Beta channel feeds are unlisted, not private.
railcast initprints a feed URL like.../appcast.xml?channel=beta&token=<beta_token>— anyone with that URL can read the beta feed, there's no per-user auth on it. Treat the URL itself as the secret; it's not shown again afterinit, but you can find the current one on the dashboard. - Tokens are account-wide, not per-app — one token can publish to every app under your account. Revoke a leaked token from the dashboard immediately; publishing continues to work for anyone with a different valid token.
- Self-hosting: override the API base URL with
--base-urlor$RAILCAST_BASE_URLif you're not using the hosted instance.
In active development. Sparkle support is live; WinSparkle and Velopack (Windows / .NET) are planned next.
Free and open source under AGPL-3.0 — self-host it, or use the hosted instance at railcast.casablanque.com. No account gating, no paid tier. Donations are welcome but never required — see the site for links.
Questions or bugs: casablanque@proton.me