Docs Ship
Alpha documentation — APIs may change between 0.x releases.

Updates

Vidra apps update in two tiers. Use either, both, or neither.

ShipsMechanismNeeds
Over-the-airyour ui/ builda new bundle, applied on the next launchnothing extra
Nativethe whole app, native code includedVelopack replaces the install in placeVidra.Updates.Native + vpk

Web bundles are small, need no installer, and can never get ahead of the binary: they install only when their generated contract fingerprints match the running host. Native updates are for releases that change C#.

Both update tiers are off until configured, and both read one vidra.updates block.

Turn updates on

Opt in from MauiProgram.cs:

builder
    .UseMauiApp<App>()
    .UseVidra()
    .UseVidraUpdates();

Then configure a feed in the app’s package.json:

{
  "vidra": {
    "updates": {
      "feedUrl": "https://updates.example.com/bundles.json"
    }
  }
}

vidra build stamps that block into the app. Optional keys include channel, which filters feed entries, and enabled: false, a kill switch that still lets an already downloaded bundle finish promoting.

Publish a web bundle

npm version patch
npx vidra bundle --merge-from https://updates.example.com/bundles.json

This builds ui/, writes dist/bundle-<version>-<hash>.zip, and produces a dist/bundles.json containing the new entry plus everything already published. Each entry contains generated core and app contract fingerprints.

Always merge the live index

--merge-from fetches the index you are serving before adding the release. Without it, a clean CI checkout would publish an index containing only the newest entry.

Older entries matter: when a native release changes a contract, older installed apps can only accept the older compatible bundles. If the live index cannot be fetched, publishing fails rather than silently starting empty.

When signing is enabled, vidra bundle also verifies that the index being merged was signed by a trusted key.

Upload in the right order

Two rules apply to every host:

  1. Upload archives first, then bundles.json and its signature. Otherwise a client can see an index that names an archive not yet available.
  2. Never delete old archives during sync. Older installs may still need them.

S3-compatible storage:

aws s3 cp dist/ s3://$BUCKET/stable/ --recursive --exclude "bundles.json*" \
  --cache-control "public, max-age=31536000, immutable"
aws s3 cp dist/bundles.json s3://$BUCKET/stable/ --cache-control "no-cache"
aws s3 cp dist/bundles.json.sig s3://$BUCKET/stable/ --cache-control "no-cache"

GitHub Releases can use a fixed tag:

gh release upload updates dist/bundle-*.zip --clobber
gh release upload updates dist/bundles.json dist/bundles.json.sig --clobber
# feedUrl: https://github.com/<owner>/<repo>/releases/download/updates/bundles.json

Cloudflare R2 via Wrangler:

for f in dist/bundle-*.zip; do wrangler r2 object put "$BUCKET/stable/$(basename "$f")" --file "$f"; done
wrangler r2 object put "$BUCKET/stable/bundles.json" --file dist/bundles.json --cache-control "no-cache"
wrangler r2 object put "$BUCKET/stable/bundles.json.sig" --file dist/bundles.json.sig --cache-control "no-cache"

Archive names contain a hash and can cache forever. The index should use no-cache so rollbacks reach clients.

Publish from CI

- run: npm version ${{ inputs.bump }} --no-git-tag-version
- run: npx vidra bundle --merge-from https://updates.example.com/bundles.json
  env:
    VIDRA_UPDATE_SIGNING_KEY: ${{ secrets.VIDRA_UPDATE_SIGNING_KEY }}
- run: ./scripts/upload-feed.sh

Compatibility and selection

Two independent checks decide whether a bundle installs:

QuestionAnswered by
May it run?both contract fingerprints match the host
Should it run?its version is greater than the current version

The fingerprints are SHA-256 over canonical bridge manifests: one for Vidra’s core contracts and one for app contracts. A native rebuild that changes no API keeps the same fingerprint; any contract change alters it.

If you change a [BridgeModule], [BridgeEventContract], or [JsContract], existing installs stop accepting new bundles until you ship a native release.

The host rechecks installed bundles on every launch. A bundle that no longer matches after a native update, or is older than the embedded UI, is set aside and the embedded copy serves instead. Rolling the native release back can make that bundle valid again. Byte-identical bundles are skipped even if their version is newer because vidra bundle creates deterministic archives.

Runtime lifecycle

  • On launch, the host promotes a previously downloaded bundle and chooses which directory serves. The embedded bundle always remains available.
  • A few seconds later, it checks the feed, downloads the newest compatible release, verifies its hash, extracts it, and stages it.
  • Nothing swaps during a running session. The staged bundle is promoted on the next launch.
  • A promoted bundle starts on probation. If it fails to boot twice, the app rolls back and does not retry that bundle.

State lives in <app data>/vidra/bundles/.

Sign the feed

The archive hash proves a download arrived intact. A signature over bundles.json proves that you published the index. Sign public feeds because a compromised feed host can otherwise serve both a modified index and matching archives.

Generate a P-256 key:

npx vidra keygen

Add the public half to package.json and rebuild:

{
  "vidra": {
    "updates": {
      "feedUrl": "https://updates.example.com/bundles.json",
      "publicKeys": ["MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."]
    }
  }
}

Publish a signed feed:

npx vidra bundle --sign vidra-signing-key.pem
# CI:
# VIDRA_UPDATE_SIGNING_KEY="$(cat key.pem)" npx vidra bundle

Configuring any public key makes signatures mandatory. vidra bundle refuses to publish unsigned when the app trusts a key and refuses a signing key the app does not trust.

Rotate a key

publicKeys is a list. Add the new public key beside the old one and ship a native release. After users receive it, begin signing with the new key. Remove the old key in a later release.

Protect and back up the private key like a production credential. Losing it requires a native release with a new trusted key before publishing can resume.

Feeds use ECDSA P-256 with SHA-256 and a detached DER-encoded signature over the exact bundles.json bytes.

Test a feed locally

A directory is a valid feed:

cd dist && python3 -m http.server 8099
# "feedUrl": "http://127.0.0.1:8099/bundles.json"

VIDRA_UPDATE_FEED_URL overrides the stamped feed at runtime, which is useful for pointing an existing build at staging.

Native whole-app updates

Vidra drives Velopack for releases that change native code. Install its CLI:

dotnet tool install -g vpk

Reference the native update package:

<PackageReference Include="Vidra.Updates.Native" Version="0.4.0" />

Initialize Velopack at the start of each platform entry point and opt into the service:

// Platforms/*/Program.cs, before anything else
VelopackApp.Build().UseVidraLocator().Run();
builder.UseVidra().UseVidraNativeUpdates();

Then add a native feed directory:

{
  "vidra": {
    "updates": {
      "feedUrl": "https://updates.example.com/bundles.json",
      "native": { "feedUrl": "https://updates.example.com/app/" }
    }
  }
}

The web feed names a file; the native feed names the directory that vpk writes into. Release each platform on its native OS:

npm version patch
npx vidra build --target windows --native-update
npx vidra build --target macos --native-update

Each build downloads the live feed before packing, preserving older releases and enabling deltas. Upload payloads first and the release index last.

The build keeps the default public artifact name and adds the native update outputs:

FilePurpose
dist/<App>-<version>-Setup.exeWindows installer and recommended download
dist/<App>-<version>-windows.zipVelopack portable archive
dist/<App>-<version>-macos.dmgDMG wrapping the packed macOS app
dist/release/Full packages, deltas, and releases.{channel}.json

On Windows, -windows.zip is the Velopack portable archive copied to Vidra’s standard artifact name. Setup.exe is only produced when --native-update is enabled.

Important constraints:

  • vpk refuses to republish an equal or lower version.
  • Keep full old packages; older installs and delta generation need them.
  • Velopack signs packaged Windows executables using the identity Vidra resolves.
  • On macOS, Velopack re-signs with --deep. The result verifies strictly, but notarization still requires testing with your own Apple Developer identity.
  • Mac Catalyst needs UseVidraLocator() because Velopack does not identify it as standard macOS.
  • Native and web updates do not need to coordinate. Contract checks reject a web bundle selected for an incompatible native release.

Environment reference

VariableEffect
VIDRA_UPDATE_FEED_URLOverride the web feed URL or local directory
VIDRA_UPDATE_CHANNELOverride the web update channel
VIDRA_ASSET_ROOTServe a directory directly and bypass updates
VIDRA_UPDATE_SIGNING_KEYPrivate signing key PEM for CI
VIDRA_NATIVE_UPDATE_FEED_URLOverride the native feed directory
VIDRA_NATIVE_UPDATE_CHANNELOverride the native channel
VIDRA_NATIVE_UPDATE_ENABLEDSet to 0 to disable native updates for one run
VIDRA_MACOS_KEYCHAINSelect a non-default signing keychain

vidra dev never checks for updates; it serves the Vite development server.

Upgrade an existing Windows app

Windows web assets use https://vidra.invalid/ so downloaded and embedded bundles share an origin. An app upgrading from a version that used MAUI’s previous https://appdir/ origin loses origin-scoped browser state once: localStorage, IndexedDB, cookies, and caches. The new origin remains stable after that migration. macOS is unchanged.

Current limits

  • A bundle counts as booted once its JavaScript constructs the Vidra client. Application code that throws after that point does not trigger rollback.
  • Web bundles are full archives; there are no delta bundles or staged rollouts.
  • Vidra does not provide an in-app update UI. Native code can inspect the running version through IVidraUpdates.