- name
- shiny-appdevicebridge
- description
- Generate code using Shiny.AppDeviceBridge, device bridges on your app's own Shiny.Net.HttpServer that also host a web app (Blazor WebAssembly, React, Vue, any static build) inside a .NET MAUI app on Android, iOS, Mac Catalyst, Windows and the maui-labs macOS and Linux heads — served from a loopback HTTP server, updated over the air from a signed release server, GitHub releases or a provider of your own, and given device access through bridges with typed C# and TypeScript clients
- auto_invoke
- true
- triggers
- ["Shiny.AppDeviceBridge","AppDeviceBridge","UseAppDeviceBridge","AddAppDeviceBridge","AppDeviceBridgeBuilder","MauiAppDeviceBridgeBuilder","AddBridge","AppDeviceBridgeOptions","AppDeviceBridgeServer","AppDeviceBridgePolicies","AuthorizeBridges","AllowAnyCallerInDebug","BridgeCallers","IAppDeviceBridgeServerExtension","IWebAppMainThread","IWebAppBackgroundInvoker","Shiny.AppDeviceBridge.WebView","WebAppHost","WebAppHostView","WebAppHostPage","WebAppHostOptions","UseBaseline","ServeWebAppRemotely","ServeWebAppLocally","OnPrepareResponse","ContentTypeOverrides","SelectVariant","client variants","mobile and desktop client","AddWebAppReleases","MapWebAppReleases","IWebAppBridge","[Truncated]"]
# Shiny.AppDeviceBridge
You are an expert in Shiny.AppDeviceBridge. Use this skill when the user hosts a web app inside a .NET MAUI app,
calls device features from that web app, updates it over the air, or writes a bridge of their own.
**Documentation:** https://shinylib.net/appdevicebridge
## The shape of it
- `Shiny.AppDeviceBridge` is the **bridge server**, on the app's own Shiny.Net.HttpServer:
`http.AddAppDeviceBridge(bridge => …)` on the `ShinyHttpServerBuilder` from `services.AddShinyHttpServer(http => …)`.
The server's address, port, TLS, limits, authentication and the app's own endpoints are configured on that same
builder; `AppDeviceBridgeOptions` holds only the bridges' concerns (app id, mount points, allowed hosts, the bridge
policy), set with `bridge.Configure(o => …)`. `UseAppDeviceBridge` in MAUI calls `AddShinyHttpServer` for you.
- **Bridges register on the bridge builder, never on `MauiAppBuilder` or `ShinyHttpServerBuilder`.**
`AppDeviceBridgeBuilder` has `Http`, `Services`, `Options`, `Configure(Action<AppDeviceBridgeOptions>)` and
`AddBridge<TBridge>()`; creating one registers the bridge server with the built-in host, settings, files and invoke
bridges. In MAUI, both `UseAppDeviceBridge` overloads — `(bridge => …, webApp => …)` with a web app, `(bridge => …)` without — hand out a
`MauiAppDeviceBridgeBuilder` (adds `Maui`, the `MauiAppBuilder`) and do the host work once: `UseShiny()` on every head (guarded, so an
app that already calls it is fine), Shiny.Core's `IBattery` and `IConnectivity`, and the app's dispatcher registered as `IWebAppMainThread`. **Never generate
`UseShiny()` for the bridges.** Calling `UseAppDeviceBridge` again adds to the same server.
- **Two kinds of bridge package.**
- **No MAUI** — BluetoothLE, Beacons, Obd, Printers, Printing, Discovery, Wifi, HttpTransfers, Database, Jobs (plain `net10.0`), Gps
(GPS/motion), Geofencing, RegionGeofencing, Notifications, Push, Wearables, LiveActivities, InAppPurchases, GameCenter, CallDirectory, Speech, ScreenRecorder, Calendar, Contacts, Health, RpiCamera, Tunnel. They reference
only `Shiny.AppDeviceBridge`; their extensions are generic (`TBuilder AddGpsBridge<TBuilder>(this TBuilder bridge)
where TBuilder : AppDeviceBridgeBuilder`) and return the builder they were given, so they chain on either builder and
run headless.
- **MAUI** — AppLinks, Camera, Photos, Folders, Desktop (tray, quick entry), and the device and sensors bridges inside
`Shiny.AppDeviceBridge.Maui` itself. They reference
`Shiny.AppDeviceBridge.Maui`, extend `MauiAppDeviceBridgeBuilder`, and do their own MAUI registration through
`bridge.Maui` (camera control, tray icon, controls, Essentials, lifecycle events). The app calls none of that.
- `AllowWebPermissions` and `UseTrafficMonitor` stay on `MauiAppBuilder`; `AddTrafficRecorder` stays on
`ShinyHttpServerBuilder`.
- For the page's own position, prefer `navigator.geolocation` with `AllowWebPermissions(WebAppWebPermissions.Geolocation)`
over the GPS bridge. It works on every head except Linux, including macOS (AppKit), where the host answers it from
CoreLocation. Declare `NSLocationWhenInUseUsageDescription` on Apple platforms.
- Every bridge route requires `AppDeviceBridgePolicies.Bridges`, which the bridges enforce themselves.
- **There is no private server.** Never generate `o.Server`, `o.ConfigureServer`, `o.AddAuthentication` or
`o.AddAuthorization` on `AppDeviceBridgeOptions` — they do not exist. Use the builder.
- `Shiny.AppDeviceBridge.WebView` adds the **web app host** — the `UseAppDeviceBridge(bridge => …, webApp => …)`
overload (bridge delegate first, both required; `http.AddAppDeviceBridge(bridge => …, webApp => …)` without MAUI).
One delegate registers the bridges alone; a second configures the web app. It serves the web app straight from a
zip (the baseline or a signed download), shown in `WebAppHostView` / `WebAppHostPage`. The WebView trades a
one-time launch token for an HttpOnly cookie, which the host adds to the bridge policy.
Only the entry document gets `Cache-Control: no-cache`; the host sets no cache header on any other file.
`WebAppHostOptions.OnPrepareResponse` runs after that for every file (an app's own cache policy for LAN/tunnel
callers), and `ContentTypeOverrides` maps extensions (with the dot) to content types.
- Who gets the pages: the WebView (launch session) always; other machines and tunnels only with
`ServeWebAppRemotely = true`; a browser on the same device only with `ServeWebAppLocally = true`. Neither option
opens the bridges — that is `AuthorizeBridges`. Never generate a custom middleware to let a local browser in.
- **Two clients (phone and desktop) at the same URLs**: `webApp.Variants("mobile", "desktop")` plus
`webApp.SelectVariant = ctx => …` (cookie, `Sec-CH-UA-Mobile`, `User-Agent`), with one zip holding `mobile/` and
`desktop/`. Never generate a second mount point, a redirect to `/desktop`, or two web app hosts for this. The
first variant is the default; a null/unknown/throwing selector falls back to it.
- The server works without the WebView: bridges only, for callers the policy admits.
- **Moving the server at runtime** (a LAN switch, a port setting) is `await http.StopAsync(); http.Options.Address = …;
http.Options.Port = …; await http.StartAsync();` on the app's `HttpServer`. `Origin`, the WebView session and an open
tunnel follow on their own; never re-register the bridges or rebuild the host to change the port.
- **Bridges** are HTTP endpoints under `/_bridge/{name}` (the prefix is configurable) plus one Server-Sent
Events stream. One package per bridge, one extension method each on the bridge builder. Host, settings, files and
invoke are built in. `AddAppBridge()` (`app/…`, over Shiny.Core) ships with `Shiny.AppDeviceBridge`; `AddDeviceBridge()`
(`device/…`, over MAUI Essentials) and `AddSensorsBridge()` (`/_bridge/sensors`) ship with `Shiny.AppDeviceBridge.Maui`.
- **The event stream carries only named topics.** `GET /_bridge/events?topics=a,b` opens it; the first event,
`bridge.stream`, carries the id for `PUT /_bridge/events/{id}` `{ "topics": [...] }`. The host runs each topic's
native source only while a stream names it, and unhooks it when the page drops it, disconnects, or the source throws
(`bridge.error { event, message }`). The typed clients manage topics — never hand-write `new EventSource(...)` in a
page that has them.
- **Subscribe before starting a session.** BLE scans and characteristic notifications, Health listeners, the OBD
monitor, Discovery browses and dictation answer `409 not_listening` without a listener for their result event, and
stop by themselves once it has none; each sensor stops once its event has none. `await` the subscription (it
resolves once the host is delivering), then start.
- **Every bridge has a typed client.** Never generate `fetch("/_bridge/…")` or JSON-object bodies in page code;
use the bridge's client — C# for Blazor, TypeScript for everything else.
## The app (MAUI)
```csharp
builder
.UseMauiApp<App>()
.UseAppDeviceBridge(
bridge => bridge
.Configure(o => o.AppId = "field-app") // AppId, BasePath, AllowedHosts, AuthorizeBridges
.AddAppBridge()
.AddDeviceBridge()
.AddGpsBridge()
.AddGeofenceBridge()
.AddCalendarBridge()
.AddPhotosBridge()
.AddFoldersBridge(),
webApp =>
{
webApp.UseBaseline(typeof(App).Assembly, "webapp.zip"); // offline, no update provider needed
// webApp.UpdateProvider = new ReleaseServerUpdateProvider(new Uri("https://api.example.com/webapps"), "-----BEGIN PUBLIC KEY-----…");
// webApp.UpdateProvider = new GitHubReleasesUpdateProvider("https://github.com/acme/field-app");
}
);
// No web app, bridges only: the other overload, with just the bridge delegate.
// builder.UseAppDeviceBridge(bridge => bridge.Configure(o => o.AppId = "kiosk").AddGpsBridge());
// A head adding its own bridges (desktop only, say) adds to the same server:
// builder.UseAppDeviceBridge(bridge => bridge.AddTrayIconBridge());
// The server itself — address, auth, the app's own endpoints — on its builder, in any order:
builder.Services.AddShinyHttpServer(http =>
{
http.Options.Address = IPAddress.Any;
http.AddAuthentication().AddApiKey(k => k.AddKey(key, "kiosk"));
http.Configure(server =>
{
server.UseAuthentication();
server.UseAuthorization();
server.MapGet("/api/orders", ctx => …).RequireAuthorization();
});
}, autoStart: false); // MAUI runs no hosted services; UseAppDeviceBridge starts the server
public class App : Application
{
protected override Window CreateWindow(IActivationState? state) => new(new WebAppHostPage());
}
```
Updates come from `webApp.UpdateProvider` (an `IUpdateProvider`): `ReleaseServerUpdateProvider(uri, publicKey)` for the
signed ASP.NET Core release server (`Channel` for prereleases), `GitHubReleasesUpdateProvider("https://github.com/owner/repo")`
for GitHub releases (`AssetName`, `IncludePrereleases`, `TagPrefix`, `Token`, `RequiredWhen`), or a class of the app's
own: `GetUpdateInfoAsync(Version host, WebAppVersion? app, ct)` returns an `UpdateInfo` (derive it to carry a download
URL; null = nothing newer) and `DownloadAsync(update, ct)` returns the zip stream. The host enforces newer-only, the
size/`Sha256` when given, and the archive opening; an exception is offline, `InvalidDataException` rejects. Never
generate `webApp.UpdateServer`, `PublicKey`, `Channel` or `HttpMessageHandlerFactory` — those options don't exist.
- `UseAppDeviceBridge` listens on loopback port 5780 unless the app sets a port of its own.
- Without MAUI: `services.AddShinyHttpServer(http => http.AddAppDeviceBridge(bridge => bridge.Configure(o => …).AddRpiCameraBridge().AddTunnel().AddBridge<ClipboardBridge>()))`.
A non-MAUI web host is the same overload on the server's builder: `http.AddAppDeviceBridge(bridge => …, webApp => …)`.
- Bridge extensions register the Shiny service behind them. Do **not** also call `AddGps()`, `AddBluetoothLE()`,
`AddBeaconRanging()` and so on.
- A platform without an implementation answers `501`; `IHostBridge.GetInfoAsync()` lists every bridge with
`IsSupported`.
- Platform setup is the underlying library's: usage descriptions, manifest permissions, entitlements. Loopback
needs cleartext to `127.0.0.1` on Android and `NSAllowsLocalNetworking` on Apple platforms.
## Bridge packages
| Package | Registration (on the bridge builder) | Client package / interface |
| --- | --- | --- |
| built in | (always) | `Shiny.AppDeviceBridge.Client`: `IHostBridge`, `ISettingsBridge`, `IFilesBridge`, `ILinksBridge` |
| `Shiny.AppDeviceBridge` (the server) | `AddAppBridge(store => store.AppleAppId = …, startup => startup.Arguments.Add(…))` | `Shiny.AppDeviceBridge.Client`: `IAppBridge` (`AppJsonContext`) — info, orientation, screen (`app.screen`), settings, store (Shiny.AppStore's `IAppStore`; 501 unless `store` is passed; `OpenReviewPageAsync`), launch at login (Shiny.Core's `IAppStartupInstaller`; 409 `startup_failed`), battery (`app.battery` carries `BatteryInfo` with energy saver) and connectivity, from Shiny.Core's `IPlatform`, `IScreenInfo`, `IBattery` and `IConnectivity`. No MAUI; a host without MAUI calls `services.AddBattery()` / `AddConnectivity()` itself |
| `Shiny.AppDeviceBridge.Maui` | `AddDeviceBridge()`, `AddSensorsBridge()` | `Shiny.AppDeviceBridge.Client`: `IDeviceBridge` (`DeviceJsonContext`) — browser (`http`/`https`/`mailto`/`tel` only), map, share (text or file roots, ≤ 20), haptics, vibrate (≤ 5 s), keep-awake (`PUT` 501 where it does not hold), clipboard (≤ 1 MB), over MAUI Essentials, each 501 alone where missing; `ISensorsBridge` — start a sensor with a speed and `MinIntervalMs`, readings only as events (`OnCompassAsync`, …), each stopped once nothing listens to its event |
| `.Gps` | `AddGpsBridge()`, `AddMotionActivityBridge()`, `AddGeocodingBridge()` | `IGpsBridge`, `IMotionBridge`, `IGeocodingBridge` (`GpsJsonContext`): `ReverseGeocodeAsync(latitude, longitude)` returns `Placemark`s from the platform geocoder (MapKit on iOS/Mac Catalyst, Android's Geocoder) or OpenStreetMap's Nominatim everywhere else, so it works on every platform; `services.AddGeocoding(o => o.BaseUri = …)` before the bridge points Nominatim at your own server. It needs network but no location permission, and fails with 503 `geocoder_unavailable` when offline. Address → position is the Maps package's `IDirectionsBridge.GeocodeAsync`, not this |
| `.Geofencing` | `AddGeofenceBridge()` | `IGeofencesBridge` (`GeofencingJsonContext`) |
| `.RegionGeofencing` | `AddRegionGeofenceBridge(cfg => cfg.AddGeocoderRegionSet(name, GeocoderRegionLevel.X) / .AddDocumentRegionSet<T>(name, idSelector, nameSelector, withinMeters:, filter:) / .AddNorthAmericanRegions() / .AddNorthAmericanCities(withinMeters:) / .AddRegionSet(name, IRegionSource), o => { o.UseDelegate<T>(); o.RegionSerializerOptions; })` | `IRegionGeofencesBridge` (`RegionGeofencingJsonContext`): `GetStatusAsync`, `RequestAccessAsync`, `StartAsync`, `StopAsync`, `GetCurrentAsync`, `OnChangeAsync` (`regiongeofence.change`; background.js `regiongeofence`). Backed by Shiny.Geofencing.Regions; region sets are registered in C# only, and the app must not also call `AddRegionGeofencing`. Document sets (`AddDocumentRegionSet`, `AddNorthAmerican*`) need the app to reference Shiny.Geofencing.Regions.DocumentDb and register a SQLite store with the geometry mapped (`MapSpatialProperty` / `MapNorthAmericaData()`, plus `AddNorthAmericaSeeder()` for the dataset). `region`: a geocoder set's placemark always; anything else only through `o.RegionSerializerOptions` metadata (pass the store's `JsonSerializerOptions`), null without it. `start`: 409 `geofence_refused`. Shares Shiny.Gps' listener with the GPS bridge. Android, iOS, Mac Catalyst; 501 elsewhere |
| `.BluetoothLE` | `AddBluetoothLEBridge()` | `IBluetoothLEBridge` |
| `.Beacons` | `AddBeaconsBridge(BeaconFeatures.All, options)`: pass flags to register only some features; a feature left out, or one the platform reports `NotSupported` (iBeacon ranging and monitoring on macOS), answers 501 | `IBeaconsBridge` (`BeaconsJsonContext`): ranging and Eddystone scans need their event listened to first (`OnBeaconAsync`, `OnEddystoneAsync`), otherwise 409 `not_listening`, and stop with the last listener. Monitoring transitions go to `OnRegionAsync` and the `beacon` native call |
| `.Obd` | `AddObdBridge()` | `IObdBridge` |
| `.Printers` | `AddPrintersBridge(o => o.NetworkPorts.Add(9200))` — `PrintersBridgeOptions.NetworkPorts` (9100–9102); registers BLE, mDNS and the PDF renderer itself | `IPrintersBridge` (`PrintersJsonContext`) + the `PrintElements` builder — ESC/POS / TSPL receipt printers over BLE or TCP; see Printing below |
| `.Printing` | `AddPrintingBridge(o => o.MaxFileBytes = …)` — `PrintingBridgeOptions`: `MaxJsonBytes` (16 MB), `MaxFileBytes` (512 MB) | `IPrintingBridge` (`PrintingJsonContext`) — the OS print dialog / spooler for PDF, image, HTML; see Printing below |
| `.Wifi` | `AddWifiBridge(hotspot)` | `IWifiBridge` |
| `.Discovery` | `AddDiscoveryBridge(protocols)` | `IDiscoveryBridge` |
| `.Push` | `AddPushBridge()` | `IPushBridge` |
| `.Wearables` | `AddWearablesBridge(o => o.Folder = "watch")` — `WearablesBridgeOptions`: `Root` (`data`), `Folder` (`wearables`), `RegisterWearableService` (on) | `IWearablesBridge` — the companion Apple Watch / Wear OS app via Shiny.Wearables 5.9; iOS and Android only, `501` elsewhere |
| `.LiveActivities` | `AddLiveActivitiesBridge(o => o.ChannelName = "Deliveries")` — Shiny's `LiveActivityOptions`; iOS also needs `<ShinyLiveActivityWidget>true</ShinyLiveActivityWidget>` | `ILiveActivitiesBridge` — iOS Live Activities and Android Live Updates |
Auf GitHub ansehen