Skip to content
Shiny.NET

Getting Started

Ship a web app — Blazor WebAssembly, React, Vue, anything that builds to static files — inside a .NET MAUI app, served from the device itself, updated from your own server, and able to call native services.

  • Served locally. A loopback Shiny.Net.HttpServer serves the app straight out of its zip. Nothing is extracted, and it works offline.
  • Updated from your server. At launch the host asks a Shiny.AppDeviceBridge.AspNetCore server whether the installed version is still acceptable. A required update downloads before the app shows. An optional one downloads in the background and applies next launch.
  • Verified. Every release is signed with ECDSA P-256 and checked against a public key compiled into the app, then checked against its SHA-256 and size before it can be served. Old releases are never reinstalled.
  • Bridged. Native services become same-origin HTTP endpoints under /_bridge, plus one Server-Sent Events stream. Each bridge is its own package and takes one extension method on the bridge builder.
  • Every head. Android, iOS, Mac Catalyst and Windows, plus the maui-labs macOS (AppKit) and Linux (GTK4) backends.

Coming from Blazor Hybrid? See AppDeviceBridge vs. Blazor Hybrid for how the two models differ and when to pick each.

┌─ MAUI app ───────────────────────────────────────────────────┐
│ WebAppHostView ── WebView ──▶ http://127.0.0.1:5780 │
│ │ │
│ AppDeviceBridgeServer ── your app's Shiny.Net.HttpServer │
│ ├─ bridge policy on this device · launch session │
│ ├─ /_bridge/* device · location · BLE · push · … │
│ ├─ /_bridge/events Server-Sent Events │
│ └─ WebAppHost ◀─ ZipFileSource ◀─ baseline or download │
│ ▲ │
└──────────────────────────────────────────────┼───────────────┘
│ signed release
Shiny.AppDeviceBridge.AspNetCore server
GitHub GitHub stars for shinyorg/webapphost
Downloads NuGet downloads for Shiny.AppDeviceBridge
Frameworks
.NET MAUI
Operating Systems
Android
iOS
macOS
Windows
Linux
claude plugin marketplace add shinyorg/skills
claude plugin install shiny@shiny

One plugin installs all 39 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

copilot plugin marketplace add https://github.com/shinyorg/skills
copilot plugin install shiny@shiny

One plugin installs all 39 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.

View shiny-appdevicebridge Skill
Package Use it in What it does
Shiny.AppDeviceBridge.Maui the app UseAppDeviceBridge(bridge => …): AddShinyHttpServer with the bridges on it, started with the app and restarted on resume; MauiAppDeviceBridgeBuilder, which the MAUI bridges extend; it also calls UseShiny() and supplies the app’s UI thread; UseTrafficMonitor and TrafficMonitorPage for debugging — see Traffic monitor
Shiny.AppDeviceBridge.WebView the app UseAppDeviceBridge(bridge => …, webApp => …), WebAppHostView, WebAppHostPage, AllowWebPermissions: the web app in a WebView, over-the-air updates, the dev server proxy, the launch session and background.js
Shiny.AppDeviceBridge.Blazor the Blazor WebAssembly app AddWebAppHostClient(): the page’s transport, typed clients for the built-in bridges (IHostBridge, ISettingsBridge, IFilesBridge, ILinksBridge), WebAppEvents, WebAppNativeCalls (typed C# handlers for jobs, GPS, geofences and push), and WebAppBridge for endpoints of your own
Shiny.AppDeviceBridge.Client (dependency) the typed-client foundation: IBridgeTransport, BridgeException, the [BridgeClient] attributes and the generator that implements them, and the built-in bridges’ contracts
Shiny.AppDeviceBridge.{Bridge}.Client the web app one per bridge: its contracts and typed client — ICalendarBridge, IWifiBridge, … — registered with Add{Bridge}BridgeClient()
@shinyorg/appdevicebridge a JavaScript or TypeScript web app the same typed clients in TypeScript, generated from the same declarations (clients/typescript)
Shiny.AppDeviceBridge (dependency) the bridge server: http.AddAppDeviceBridge(bridge => …) on Shiny.Net.HttpServer’s ShinyHttpServerBuilder, AppDeviceBridgeBuilder (Configure, AddBridge<T>()), AppDeviceBridgeOptions (mount points, allowed hosts, the bridge policy), bridge contracts, built-in settings, files and native-call endpoints; no MAUI dependency
Shiny.AppDeviceBridge.Tunnel the app, or a headless device bridge.AddTunnel(): a public HTTPS address for the server, opened and closed while the app runs; everything through it is treated as a remote caller
Shiny.AppDeviceBridge.Simulator a .NET tool shiny-bridge-sim: a terminal UI (or, with --web, a browser panel) that serves every bridge with answers you set, events you fire and trails you play (GPX walks included), with a traffic monitor, and an MCP endpoint an AI agent can drive it through — test a page without a device. See Simulator
Shiny.AppDeviceBridge.Core (dependency) protocol contracts, version ordering, release signatures
Shiny.AppDeviceBridge.AspNetCore your server AddWebAppReleases, MapWebAppReleases, file-system release store
Shiny.AppDeviceBridge.AppSupport the app AddAppSupportBridge() — device info, orientation, browser, maps, settings, app store, launch at login, share, haptics and vibration, connectivity, battery, screen and clipboard; and /_bridge/sensors — accelerometer, gyroscope, magnetometer, compass, barometer and orientation
Shiny.AppDeviceBridge.AppSupport.Linux the Linux (GTK4) head AddAppSupportLinux(): battery and energy saver for AddAppSupportBridge() from UPower and power-profiles-daemon over D-Bus, with change events
Shiny.AppDeviceBridge.Gps the app AddGpsBridge(), AddMotionActivityBridge(): GPS and motion activity, backed by Shiny.Gps
Shiny.AppDeviceBridge.Geofencing the app AddGeofenceBridge(): geofence regions and transitions, backed by Shiny.Geofencing
Shiny.AppDeviceBridge.BluetoothLE the app AddBluetoothLEBridge()
Shiny.AppDeviceBridge.Beacons the app AddBeaconsBridge(BeaconFeatures.All): iBeacon ranging and background region monitoring, Eddystone scanning, and broadcasting as a beacon, backed by Shiny.Beacons
Shiny.AppDeviceBridge.Obd the app AddObdBridge(): OBD-II over Bluetooth LE or Wi-Fi adapters — decoded PIDs, VIN, trouble codes, live readings
Shiny.AppDeviceBridge.Printers the app AddPrintersBridge(): ESC/POS and TSPL receipt and label printers over Bluetooth LE or the network (raw TCP 9100, found over mDNS) — receipts the page builds, rendered to PDF too, backed by Shiny.Printers. See Printing
Shiny.AppDeviceBridge.Printing the app AddPrintingBridge(): the operating system’s own printing — a PDF, image or HTML through AirPrint, Android’s print dialog, the Windows spooler or CUPS — backed by Shiny.Printing. See Printing
Shiny.AppDeviceBridge.Wifi the app AddWifiBridge(hotspot: false): current network and changes, scan, connect, known networks, radio, hotspot
Shiny.AppDeviceBridge.Discovery the app AddDiscoveryBridge(DiscoveryProtocols.All): mDNS/Bonjour, SSDP/UPnP, WS-Discovery search, browse, resolve and publish
Shiny.AppDeviceBridge.Jobs the app AddWebAppJob(name, configure): background jobs handled by the page or background.js
Shiny.AppDeviceBridge.Push the app AddPushBridge(): register, unregister, token, tags, and optionally push payloads for the web app
Shiny.AppDeviceBridge.Wearables the app AddWearablesBridge(): the companion Apple Watch or Wear OS app, through Shiny.Wearables — status, live messages the page or background.js answers, shared context, queued transfers and files through the file roots (iOS and Android; 501 elsewhere). See Wearables
Shiny.AppDeviceBridge.LiveActivities the app AddLiveActivitiesBridge(): iOS Live Activities and Android 16 Live Updates, through Shiny.Mobile.LiveActivities — start, update and end them from the page, with their push tokens handed to the page or background.js (iOS and Android; 501 elsewhere)
Shiny.AppDeviceBridge.InAppPurchases the app AddInAppPurchasesBridge(): App Store (StoreKit 2) and Google Play Billing purchases, through Shiny.Mobile.InAppPurchases — products, the store’s purchase sheet, entitlements, finishing, restores and subscription management, with purchase updates handed to the page or background.js (iOS and Android; 501 elsewhere)
Shiny.AppDeviceBridge.Maps the app AddMapsBridge(): vector map tiles online or from regions the user downloads (verified against a signed catalog), live traffic flow and incidents from pluggable providers (TomTom, HERE and Azure Maps built in), turn-by-turn directions from an online Valhalla router, and addresses turned into stops by a pluggable geocoder (Nominatim built in) (all platforms). See Maps & Directions
Shiny.AppDeviceBridge.Maps.Valhalla the app AddOnDeviceDirections(): directions computed on the phone over a downloaded region’s road network (Android and iOS; online elsewhere)
Shiny.AppDeviceBridge.Maps.Blazor the web app <BridgeMap>: MapLibre GL JS bundled for offline use, with pins, lines, areas, circles, click-to-draw and routes
Shiny.AppDeviceBridge.MapPacks a tool shiny-map-packs: builds downloadable regions — the map cut from a PMTiles planet, the road network built with Valhalla
Shiny.AppDeviceBridge.Notifications the app AddNotificationsBridge(): local notifications now, scheduled, repeating or at a geofence; pending, cancel, badge, channels; taps handed to the web app
Shiny.AppDeviceBridge.HttpTransfers the app AddHttpTransfersBridge(): background uploads and downloads to and from file roots, with progress events and completion handlers
Shiny.AppDeviceBridge.AppLinks the app AddAppLinksBridge(o => o.Schemes.Add("myapp")): deep links and universal/app links routed to the page
Shiny.AppDeviceBridge.Health the app AddHealthBridge(): HealthKit and Health Connect permissions, bucketed reads, writes and live readings
Shiny.AppDeviceBridge.Speech the app AddSpeechBridge(): on-device speech recognition, dictation as events, text-to-speech, voices
Shiny.AppDeviceBridge.ScreenRecorder the app AddScreenRecorderBridge(): record the device’s screen to a video filed into a file root, through Shiny.ScreenRecorder — microphone and system audio where the platform has them, pause and resume, a time limit, and events for every state change and ending (all platforms)
Shiny.AppDeviceBridge.Contacts the app AddContactsBridge(): access, paged search, read, photos, create, update and delete (Android, iOS)
Shiny.AppDeviceBridge.Calendar the app AddCalendarBridge(): access, calendars, events in a date range, create, update and delete
Shiny.AppDeviceBridge.Camera the app AddCameraBridge(): this device’s own camera driven from a page anywhere — a live MJPEG viewfinder, photos and video filed into a file root, lens, zoom, torch and effects; CameraBridgeView for a camera screen of your own
Shiny.AppDeviceBridge.Photos the app AddPhotosBridge(): the system photo picker, and the photo library — pages, thumbnails and full-size exports — as files in a file root
Shiny.AppDeviceBridge.Folders the app AddFoldersBridge(): the platform’s folder picker, and FolderRoots for folders the app adds by path — each remembered as a file root across launches
Shiny.AppDeviceBridge.Desktop the app AddTrayIconBridge(): system tray / menu bar icons, menus, badges, notifications and animation. AddQuickEntryBridge(): a prompt window that opens over other applications from a global hotkey. Both hand what the user does back to the web app
Shiny.AppDeviceBridge.RpiCamera the app, or a headless Pi bridge.AddRpiCameraBridge(), on a MAUI or headless bridge builder: Raspberry Pi cameras through libcamera — snapshots, captures into a file root, sensor controls and a shared live MJPEG stream; camera.StreamToAsync(stream) streams framed JPEGs into any Stream, such as a Bluetooth LE L2CAP channel, for ReadFramesAsync to read

AppSupport, AppSupport.Linux, AppLinks, Camera, Photos, Folders and Desktop need MAUI: they reference Shiny.AppDeviceBridge.Maui and do their own MAUI registration. The rest — BluetoothLE, Beacons, Obd, Printers, Printing, Discovery, Wifi, HttpTransfers, Jobs, Gps, Geofencing, Notifications, Push, Wearables, LiveActivities, InAppPurchases, Speech, ScreenRecorder, Calendar, Contacts, Health, RpiCamera and Tunnel — reference only Shiny.AppDeviceBridge, so they also run without MAUI, on a headless device.

builder
.UseMauiApp<App>()
.UseAppDeviceBridge(
bridge => bridge
.Configure(o => o.AppId = "field-app")
.AddAppSupportBridge()
.AddGpsBridge()
.AddGeofenceBridge()
.AddBluetoothLEBridge(),
webApp =>
{
webApp.UseBaseline(typeof(App).Assembly, "webapp.zip", "1.0.0"); // runs offline on first launch
webApp.UpdateProvider = new GitHubReleasesUpdateProvider("https://github.com/acme/field-app");
}
);

With a second delegate for the web app’s options, UseAppDeviceBridge serves the web app too. Its first delegate gets the bridge builder: Configure sets the bridge server’s options, and each bridge package adds one extension to it. Host, settings, files and native calls are built in; everything else — AddAppSupportBridge() included — is a package you add.

public class App : Application
{
protected override Window CreateWindow(IActivationState? state) => new(new WebAppHostPage());
}

Updates are optional. Without an UpdateProvider nothing is checked or downloaded, and the app simply serves the zip compiled into it — which is a complete setup on its own:

builder
.UseAppDeviceBridge(
bridge => bridge.Configure(o => o.AppId = "field-app"),
webApp => webApp.UseBaseline(typeof(App).Assembly, "webapp.zip")); // version defaults to 1.0.0

No web app at all? The other UseAppDeviceBridge overload takes only the bridge delegate and serves the bridges to callers on the device (any caller in a debug build); the server’s own builder and AuthorizeBridges decide the rest.

builder.UseAppDeviceBridge(bridge => bridge
.Configure(o => o.AppId = "kiosk")
.AddGpsBridge());

Calling UseAppDeviceBridge again adds to the same server — a desktop head adding its own bridges, say: builder.UseAppDeviceBridge(bridge => bridge.AddTrayIconBridge()). See Security.

There is no manifest, no signing key and no network at any point; the install directory is never even created. Add an UpdateProvider later and the embedded build becomes the floor that downloads are compared against, which is when the version argument starts to matter.

Each bridge extension also registers the Shiny service behind it, and UseAppDeviceBridge calls UseShiny() for you (once — an app that already calls it is fine). Don’t add AddGps(), AddGeofencing() or AddBluetoothLE() yourself. Where a platform has no implementation, that bridge’s endpoints return 501 and GET /_bridge/host reports it as unsupported.

Embed the baseline zip with a LogicalName:

<EmbeddedResource Include="webapp.zip" LogicalName="webapp.zip" />

The zip can hold the files at its root or under wwwroot/. A zipped Blazor publish works either way, and its precompressed .br/.gz files are served as they are. Only the entry document is sent with Cache-Control: no-cache, so an update is never hidden behind a cached index.html; every other file carries no cache header from the host. OnPrepareResponse runs after that for every file, so an app serving the pages over a LAN or a tunnel can cache fingerprinted assets for a year and still leave the entry document to revalidate:

webApp.OnPrepareResponse = x =>
{
// Your rule for which names carry a content hash, such as dotnet.runtime.zbexyp8zrs.js.
if (MyCachePolicy.IsFingerprinted(x.File.Name))
x.HttpContext.Response.Headers["Cache-Control"] = "public, max-age=31536000, immutable";
};
Platform Required
Android Cleartext to 127.0.0.1: a network security config (see the sample) or usesCleartextTraffic
iOS / Mac Catalyst NSAppTransportSecurity → NSAllowsLocalNetworking
Mac Catalyst, sandboxed macOS com.apple.security.network.server entitlement

Plus the usage descriptions and permissions for whichever bridges you add.

Every bridge has a typed client — see Typed clients. A Blazor WebAssembly app registers the ones it uses and injects them:

builder.Services
.AddWebAppHostClient()
.AddAppBridgeClient()
.AddGpsBridgeClient();
@inject IAppBridge App
var info = await App.GetInfoAsync();

Any other web app uses the same clients from @shinyorg/appdevicebridge:

import { AppBridge } from "@shinyorg/appdevicebridge";
const info = await new AppBridge().getInfo();

The repository’s Blazor sample, in the macOS (AppKit) head. Bridges macOS has no implementation for are reported as unsupported, and the Device page reads the real machine:

The Blazor sample inside the macOS app: platform macos, 19 of 26 bridges, with contacts, geofences, gps and health not on this platform The sample's Device page in the macOS app, showing GET app/info for the Mac it runs on

To build the page without a device at all, run it against the simulator.