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 | |
| Downloads |
Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne 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.
Step 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install the plugin:
copilot plugin install shiny@shinyOne 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.
Packages
Section titled “Packages”| 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.
The app
Section titled “The app”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.0No 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 setup
Section titled “Platform setup”| 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.
The web app
Section titled “The web app”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:
![]() |
![]() |
To build the page without a device at all, run it against the simulator.




