Automatic updates for any application. Built in Rust. Ships in 5 minutes.
Why Surge • 5-Minute Setup • CI/CD • How It Works • Features • Integration • Reference • Building
Your users should always be on the latest version. Chrome, VS Code, and Slack do this transparently — the app checks for updates, downloads a small patch, and applies it. The user never thinks about it.
Building that yourself means solving a dozen hard problems: hosting an update server, generating delta patches, handling partial downloads, supporting multiple platforms, managing release channels, coordinating deployments across servers, preserving user data across updates, creating installers, setting up shortcuts. Most teams either skip it entirely or ship a half-baked updater that breaks silently.
Surge gives you Chrome-style automatic updates for any application, on any platform, in about 5 minutes.
- No update server to run. Releases are stored directly in S3, Azure Blob, GCS, GitHub Releases, or a plain directory. You already have one of these.
- No framework lock-in. Surge is a native shared library with a stable C ABI. Call it from Rust, C, C++, .NET, Go, Python — anything that can load a
.soor.dll. - Small downloads. Binary delta patches (bsdiff + zstd) mean users download only what changed between versions. Typically 5-20% of the full package.
- Release channels. Ship to
betafirst, thenpromotethe exact same build tostablewhen you're confident. No rebuild, no re-upload. - User data survives updates. Mark config files, databases, and user content as persistent assets — Surge preserves them across every version.
- Fits your CI pipeline.
surge packandsurge pushare plain CLI commands. Add them to GitHub Actions, GitLab CI, or Jenkins — works in any matrix build across OS, architecture, and build variants. - Cross-platform from day one. Linux, Windows, and macOS. Native shortcuts (.desktop files, .lnk files, .app bundles), platform-correct install directories, and architecture detection built in.
You need two things: somewhere to store your releases and the surge CLI.
surge init --wizardThe wizard walks you through storage provider, app name, and target platform. Or do it non-interactively:
surge init \
--app-id my-app \
--name "My App" \
--provider s3 \
--bucket my-app-releasesThe result is a surge.yml manifest:
schema: 1
storage:
provider: s3
bucket: my-app-releases
region: us-east-1
apps:
- id: my-app
name: My App
main: my-app
target:
rid: linux-x64Credentials are never stored in the manifest. Surge reads them from environment variables (AWS_ACCESS_KEY_ID, GITHUB_TOKEN, etc.) or IAM roles.
Point Surge at your build output:
surge pack \
--app-id my-app \
--rid linux-x64 \
--version 1.0.0By default, surge pack reads artifacts from .surge/artifacts/<app-id>/<rid>/<version>, writes packages to
.surge/packages, and writes installers to .surge/installers/<app-id>/<rid>. Use --artifacts-dir/--output-dir
to override.
Surge compresses everything into a tar.zst package. If a previous version exists in storage, it also generates a
binary delta patch automatically.
surge push \
--app-id my-app \
--rid linux-x64 \
--version 1.0.0 \
--channel stableDone. Your release is live. Clients on the stable channel will pick it up on their next update check.
Install from the backend configured in .surge/application.yml (falls back to .surge/surge.yml):
surge install \
--channel stableOverride backend fields without editing manifest:
surge install backend \
--provider s3 \
--bucket my-release-bucket \
--region eu-north-1 \
--prefix productionInstall to a remote node on your tailnet:
surge install tailscale \
--node my-node \
--ssh-user operator \
--channel stableThis command:
- probes remote OS/architecture and checks for NVIDIA GPU support,
- resolves the newest matching release on the selected channel,
- downloads it locally and sends it with
tailscale file cp.
Use --plan-only to preview selection without transfer, or --rid to force a specific RID. If your tailnet
requires explicit SSH identity, pass --ssh-user <account> (or set --node <account>@<node> directly).
.NET
using var mgr = new SurgeUpdateManager();
await mgr.UpdateToLatestReleaseAsync(
onUpdatesAvailable: releases =>
Console.WriteLine($"{releases.Count} update(s), latest: {releases.Latest?.Version}"),
onAfterApplyUpdate: release =>
Console.WriteLine($"Updated to {release.Version}")
);Rust
let mut mgr = UpdateManager::new(ctx, "my-app", "1.0.0", "stable", install_dir)?;
if let Some(info) = mgr.check_for_updates().await? {
mgr.download_and_apply(&info, None::<fn(_)>).await?;
}C / C++ / anything else
surge_update_manager* mgr = surge_update_manager_create(ctx, "my-app", "1.0.0", "stable", dir);
surge_releases_info* info = NULL;
if (surge_update_check(mgr, &info) == SURGE_OK)
surge_update_download_and_apply(mgr, info, progress_cb, NULL);Surge is built for automated pipelines. The CLI does all the heavy lifting — your CI just calls surge pack and surge push after each build. GitHub Actions is the most common setup.
# .github/workflows/release.yml
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: cargo build --release
- run: surge pack --version ${{ env.VERSION }}
- run: surge push --version ${{ env.VERSION }} --channel stableReal applications target multiple OS and architecture combinations. Use a matrix strategy to build each variant in parallel, then pack and push each one:
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
rid: linux-x64
- os: windows-latest
rid: win-x64
- os: macos-latest
rid: osx-arm64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- run: dotnet publish -c Release -r ${{ matrix.rid }}
- run: surge pack --rid ${{ matrix.rid }} --version ${{ env.VERSION }}
- run: surge push --rid ${{ matrix.rid }} --version ${{ env.VERSION }} --channel stableEach matrix entry produces its own platform-specific package and delta patch. Clients only download the package matching their OS and architecture.
Combine matrix builds with channel promotion for safe deployments:
jobs:
deploy-beta:
needs: [build]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- run: surge push --version ${{ env.VERSION }} --channel beta
promote-stable:
needs: [build]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- run: surge promote --version ${{ env.VERSION }} --from beta --to stablePush to develop ships to beta testers. Merge to main promotes the exact same build to stable — no rebuild, no re-upload, no risk of a different binary reaching production.
When multiple matrix jobs push to the same storage backend, use the distributed lock to prevent race conditions on the release index:
steps:
- run: surge lock acquire --name "${{ matrix.rid }}-deploy"
- run: surge push --version ${{ env.VERSION }} --rid ${{ matrix.rid }} --channel stable
- run: surge lock release --name "${{ matrix.rid }}-deploy" You (developer) Your Users
────────────── ──────────
cargo build / dotnet publish
│
▼
surge pack ──► tar.zst full package
+ bsdiff delta patch
│
▼
surge push ──► S3 / Azure / GCS / GitHub Releases / filesystem
│
│ release index (compressed YAML)
│ + package files
│
▼
┌──────────────┐
│ Cloud Storage │
└──────┬───────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
Linux Windows macOS
app app app
│ │ │
└───────────┴───────────┘
│
check_for_updates()
download_and_apply()
│
▼
Update applied.
User never noticed.
When a client calls download_and_apply, Surge runs a 6-phase pipeline:
- Check — validate update info and prepare staging directory
- Download — fetch delta patch (or full package as fallback) from storage
- Verify — SHA-256 hash check of every downloaded file
- Extract — decompress the tar.zst archive
- Apply delta — apply bsdiff patches if using delta updates
- Finalize — atomic move into place, clean up staging, preserve persistent assets
Progress callbacks fire at each phase with percentage, bytes transferred, and speed.
Channels are labels on releases. A single version can be on multiple channels simultaneously.
# Ship to beta testers first
surge push --version 2.1.0 --channel beta
# A week later, promote the exact same build to stable (no re-upload)
surge promote --version 2.1.0 --from beta --to stable
# Something wrong? Pull it back
surge demote --version 2.1.0 --channel stableClients specify which channel they follow. Switching channels at runtime is a single API call — useful for opt-in beta programs.
Files and directories that should survive across updates:
apps:
- id: my-app
persistentAssets:
- config.json
- user-data/
- settings.iniDuring updates, Surge copies these from the old version directory to the new one before removing the old version.
apps:
- id: my-app
icon: icon.png
shortcuts:
- desktop
- start_menu
- startupSurge creates real platform shortcuts:
- Linux —
.desktopfiles in~/.local/share/applicationsand~/.config/autostart(XDG freedesktop spec) - Windows —
.lnkshortcuts on Desktop, Start Menu, and Startup viaWScript.Shell - macOS —
.appbundles withInfo.plistin~/Applications, LaunchAgent for startup
The supervisor binary monitors your application, restarts on crash, and coordinates version handoffs:
surge-supervisor --supervisor-id <uuid> --install-dir /opt/my-app --exe-path /opt/my-app/my-appOr from code:
SurgeApp.StartSupervisor();It handles graceful shutdown on SIGTERM/SIGINT (Unix) and Ctrl+C (Windows).
Hook into first-run, post-install, and post-update events:
SurgeApp.ProcessEvents(args,
onFirstRun: v => ShowWelcomeScreen(),
onInstalled: v => RunMigrations(),
onUpdated: v => ShowChangelogFor(v));Surge can produce installer bundles in two modes:
target:
rid: win-x64
installers:
- web # Small bootstrap, downloads app on first run
- offline # Self-contained, includes full packageThrottle resource usage for constrained environments:
var budget = new SurgeResourceBudget {
MaxMemoryBytes = 256 * 1024 * 1024, // 256 MB
MaxConcurrentDownloads = 2,
MaxDownloadSpeedBps = 1_000_000, // 1 MB/s
ZstdCompressionLevel = 6 // faster compression
};For server-side deployments where multiple CI runners might push releases concurrently, Surge provides a distributed mutex via snapx.dev:
surge lock acquire --name "my-app-deploy" --timeout 300
# ... push release ...
surge lock release --name "my-app-deploy"Move all your releases from one storage provider to another without downtime:
surge migrate --dest-manifest new-backend.ymlUse whatever you already have.
| Provider | Config value | Notes |
|---|---|---|
| Amazon S3 | s3 |
Any S3-compatible API (MinIO, Cloudflare R2, DigitalOcean Spaces). Auth via AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY or IAM roles |
| Azure Blob Storage | azure_blob |
Auth via AZURE_STORAGE_ACCOUNT_NAME/AZURE_STORAGE_ACCOUNT_KEY |
| Google Cloud Storage | gcs |
Auth via GOOGLE_APPLICATION_CREDENTIALS or application default credentials |
| GitHub Releases | github_releases |
Free for public repos. bucket = owner/repo. Auth via GITHUB_TOKEN |
| Local filesystem | filesystem |
For testing or air-gapped environments. bucket = root directory path |
Surge is a native shared library (libsurge.so / surge.dll / libsurge.dylib) with a C ABI. You don't need Rust in your project.
The Surge.NET NuGet package provides the full API:
- netstandard2.0 —
[DllImport]for .NET Framework 4.6.1+, .NET Core, Mono, Xamarin - net10.0 —
[LibraryImport]with full AOT and trimming support - Zero external dependencies
SurgeUpdateManager.UpdateToLatestReleaseAsync()— one call that checks, downloads, verifies, extracts, and applies- Per-phase progress callbacks, cancellation tokens, pre/post-update hooks
Include surge_api.h and link against the shared library. 31 functions, all following the same pattern: opaque handles, surge_result return codes, thread-safe cancellation.
Use surge-core as a Cargo dependency for direct access to the async API without the FFI overhead.
surge init Create a surge.yml manifest (--wizard for interactive)
surge pack Build full and delta packages from artifacts
surge push Upload packages and update the release index
surge list List releases on a channel
surge promote Promote a release to another channel
surge demote Remove a release from a channel
surge migrate Copy releases between storage backends
surge restore Restore artifacts from backup
surge install Install package via method (backend, tailscale)
surge lock Acquire/release distributed locks
If the manifest has one app, --app-id is optional. If the app has one target, --rid is optional.
surge list now defaults to a status overview table. For multi-app manifests it shows one row per app/rid by default;
use --app-id (and optionally --rid) to scope down.
surge restore also supports installer-only generation (snapx-style restore -i) from existing full packages:
surge restore -iBy default this resolves the latest release for the manifest app/target and default channel, restores missing full
packages from storage into .surge/packages, and builds installers using artifacts from
.surge/artifacts/<app-id>/<rid>/<version>. The generated installers are written to
.surge/installers/<app-id>/<rid>.
Explicit override example:
surge restore -i \
--version 1.2.3 \
--artifacts-dir ./publish \
--packages-dir .surge/packages| Group | Functions |
|---|---|
| Lifecycle | surge_context_create, surge_context_destroy, surge_context_last_error |
| Configuration | surge_config_set_storage, surge_config_set_lock_server, surge_config_set_resource_budget |
| Update Manager | surge_update_manager_create, surge_update_manager_destroy, surge_update_manager_set_channel, surge_update_manager_set_current_version, surge_update_check, surge_update_download_and_apply |
| Release Info | surge_releases_count, surge_releases_destroy, surge_release_version, surge_release_channel, surge_release_full_size, surge_release_is_genesis |
| Binary Diff | surge_bsdiff, surge_bspatch, surge_bsdiff_free, surge_bspatch_free |
| Pack Builder | surge_pack_create, surge_pack_build, surge_pack_push, surge_pack_destroy |
| Distributed Lock | surge_lock_acquire, surge_lock_release |
| Supervisor | surge_supervisor_start |
| Events | surge_process_events |
| Cancellation | surge_cancel |
schema: 1
storage:
provider: s3 # s3 | azure_blob | gcs | github_releases | filesystem
bucket: my-bucket # bucket, container, owner/repo, or directory
region: us-east-1 # cloud region (or release tag for github_releases)
endpoint: "" # custom endpoint (MinIO, R2, etc.)
prefix: "" # path prefix within bucket
lock:
url: https://snapx.dev # distributed lock server (optional)
apps:
- id: my-app # unique identifier
name: My App # display name
main: my-app # main executable (defaults to id)
installDirectory: my-app # install dir name (defaults to id)
icon: icon.png # application icon
channels: [stable, beta] # supported channels
shortcuts: [desktop, start_menu, startup]
persistentAssets: [config.json, user-data/]
installers: [web, offline]
environment:
MY_VAR: value
target:
rid: linux-x64 # linux-x64, win-x64, win-arm64, osx-x64, osx-arm64Target-level settings override app-level defaults for icon, shortcuts, persistentAssets, installers, and environment.
┌──────────────────────────────────────────────────────────┐
│ Your Application │
│ (.NET / C / C++ / any FFI) │
└─────────────────────────┬────────────────────────────────┘
│ P/Invoke or C calls
┌─────────────────────────▼────────────────────────────────┐
│ surge-ffi (cdylib) │
│ 31 exported functions · surge_api.h │
└─────────────────────────┬────────────────────────────────┘
│
┌─────────────────────────▼────────────────────────────────┐
│ surge-core │
│ config · crypto · storage · archive · diff · releases │
│ update · pack · supervisor · platform · download │
└──────────────────────────────────────────────────────────┘
| Crate | Description |
|---|---|
surge-core |
Core library — config, crypto, storage backends, archive (tar+zstd), bsdiff, release index, update manager, pack builder, supervisor, platform detection |
surge-ffi |
C API shared library exporting 31 functions through surge_api.h |
surge-cli |
Command-line tool for packing, pushing, and managing releases |
surge-supervisor |
Standalone process supervisor binary |
git clone --recurse-submodules https://github.com/fintermobilityas/surge.git
cd surgeIf you already cloned without --recurse-submodules:
git submodule update --init- Rust 1.85+ (Edition 2024) — install via rustup
- .NET 10 SDK (optional, for the .NET wrapper and demo app)
cargo build --release
cargo test
cargo clippy --all-targets --all-features -- -D warnings
cargo fmt --allcd dotnet
dotnet build --configuration Release
dotnet test --configuration ReleaseMIT © 2026 Finter As