Skip to content
Shiny.NET
Commercial SupportGuaranteed response times on Shiny bug fixes, straight from the maintainers. Plans from US$1,500 a year.See the plans

Updates

Where releases come from is an IUpdateProvider on webApp.UpdateProvider. Two ship in Shiny.AppDeviceBridge.WebView:

// The release server below: every release ECDSA-signed, checked against the key compiled into the app.
webApp.UpdateProvider = new ReleaseServerUpdateProvider(new Uri("https://api.example.com/webapps"), WebAppKeys.Public)
{
Channel = "beta" // optional: follow a prerelease channel
};
// A GitHub repository's releases: the tag is the version (v1.2.0), the notes are the what's new, a .zip asset is the build.
webApp.UpdateProvider = new GitHubReleasesUpdateProvider("https://github.com/acme/field-app")
{
AssetName = "webapp-*.zip", // default: the first .zip asset
IncludePrereleases = false, // default
TagPrefix = null, // "webapp-" for webapp-v1.2.0 tags
Token = null, // private repositories, higher rate limit
RequiredWhen = r => r.WhatsNew?.Contains("[required]") == true // default: every update optional
};

GitHub releases are not signed. The provider checks the asset’s SHA-256 when GitHub reports one (digest, on assets uploaded since mid-2025). Beyond that, trust rests on HTTPS to GitHub and on who can publish to the repository. Drafts are never offered. Only the newest 100 releases are read, and GitHub Enterprise Server URLs work too. Unauthenticated API calls are limited to 60 an hour per IP address.

Anything else — a manifest file, an endpoint of your own — is two methods:

public interface IUpdateProvider
{
// Null when there is nothing newer. currentAppVersion is null when no build is bundled or installed.
Task<UpdateInfo?> GetUpdateInfoAsync(Version currentHostVersion, WebAppVersion? currentAppVersion, CancellationToken ct);
Task<Stream> DownloadAsync(UpdateInfo update, CancellationToken ct);
}
public class UpdateInfo // derive to carry a download URL
{
public required WebAppVersion Version { get; set; }
public bool IsOptional { get; set; } // false installs before the app shows
public string? WhatsNew { get; set; }
public DateTimeOffset? ReleaseDate { get; set; }
public long? FileSize { get; set; } // checked when set
public string? Sha256 { get; set; } // checked when set
}

Whatever the provider, the host keeps the same rules. It gives up on the check after CheckTimeout, and refuses a release that isn’t newer than the one it runs. It checks the size and hash when they’re given, and opens the archive, entry document included, before installing it. An exception from the provider counts as being offline. An InvalidDataException rejects the release instead. The host owns the provider and disposes it.

builder.Services.AddWebAppReleases(o =>
{
o.SigningKey = builder.Configuration["WebApps:SigningKey"]; // PEM, from your secret store
o.ReleasesDirectory = "/srv/webapps";
});
app.MapWebAppReleases("/webapps"); // returns the route group: add auth, rate limiting, caching

Publishing a release means copying a file:

/srv/webapps/field-app/
app.json { "minimumVersion": "1.2.0" }
1.2.0.zip
1.3.0-beta.1.zip
1.3.0-beta.1.json { "channel": "beta", "minimumHostVersion": "2.1", "platforms": ["ios", "android"] }

Hashes and sizes are computed and cached. Implement IWebAppReleaseStore to serve releases from blob storage or a database instead. Set downloadUrl in the sidecar to serve the zip from a CDN; the signature still covers it.

Create a key pair once, with WebAppReleaseSignature.CreateKeyPair() or openssl:

Terminal window
openssl ecparam -name prime256v1 -genkey -noout | openssl pkcs8 -topk8 -nocrypt > private.pem
openssl ec -in private.pem -pubout > public.pem
Situation Result
Nothing installed any compatible release is required.
Below minimumVersion required, and it installs before the app shows.
Newer release available optional. It downloads in the background and applies next launch, or when the page calls POST /_bridge/host/apply-update.
Release needs a newer native app skipped. Set minimumHostVersion when a web build depends on bridge endpoints older apps don’t have.
Offline or server down the newer of the bundled and installed builds is served.

With another provider, the provider decides which release is newest and whether it’s required (IsOptional). Nothing installed still means required, and offline still means the newer of the bundled and installed builds.

GET/_bridge/host#

The app, the web app build being served, and every registered bridge with whether it works here.

C#IHostBridge.GetInfoAsyncTSHostBridge.getInfo
Response
200 HostInfo
HostInfo · 8 fields
appIdstringThe app's id, as the update server knows it.
hostVersionstringThe native app's version.
platformstringandroid, ios, maccatalyst, macos, windows or linux.
webAppVersionstring | nullThe web app build being served; null while pages come from a dev server.
webAppOriginWebAppOrigin | nullWhether that build is the baseline or a download.
pendingVersionstring | nullA downloaded update waiting for applyUpdate.
bridgesBridgeCapability[]Every registered bridge.
devServerstring | nullThe dev server pages are proxied from, in a debug build that uses one.
POST/_bridge/host/apply-update#

Switches to a downloaded update, if one is waiting. The page reloads itself afterwards; the host never reloads it out from under the user.

C#IHostBridge.ApplyUpdateAsyncTSHostBridge.applyUpdate
Response
200 ApplyUpdateResult
ApplyUpdateResult · 2 fields
appliedbooleanFalse when nothing was waiting.
versionstring | nullThe version now being served, when one was applied.

JSON property names are camelCase and enums travel as their names. A ? marks a property the payload may omit;| null one that may be null.

HostInfoobject#

What is serving the page.

appIdstringThe app's id, as the update server knows it.
hostVersionstringThe native app's version.
platformstringandroid, ios, maccatalyst, macos, windows or linux.
webAppVersionstring | nullThe web app build being served; null while pages come from a dev server.
webAppOriginWebAppOrigin | nullWhether that build is the baseline or a download.
pendingVersionstring | nullA downloaded update waiting for applyUpdate.
bridgesBridgeCapability[]Every registered bridge.
devServerstring | nullThe dev server pages are proxied from, in a debug build that uses one.
WebAppOriginenum#

Where the web app being served came from.

"Baseline""Installed"
BridgeCapabilityobject#

A registered bridge.

namestringIts path under the bridge prefix — calendar for /_bridge/calendar.
isSupportedbooleanFalse where the platform has no implementation; its endpoints then answer 501.
ApplyUpdateResultobject#

The outcome of applying an update.

appliedbooleanFalse when nothing was waiting.
versionstring | nullThe version now being served, when one was applied.