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. - Every head. Android, iOS, Mac Catalyst and Windows, plus the maui-labs macOS (AppKit) and Linux (GTK4) backends.
┌─ MAUI app ───────────────────────────────────────────────────┐│ WebAppHostView ── WebView ──▶ http://127.0.0.1:5780 ││ │ ││ WebAppHost ── Shiny.Net.HttpServer (loopback only) ││ ├─ session guard launch token → HttpOnly cookie ││ ├─ /_bridge/* device · location · BLE · push · … ││ ├─ /_bridge/events Server-Sent Events ││ └─ static files ◀─ 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 37 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 37 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 | UseWebAppHost, WebAppHostView, WebAppHostPage, AllowWebPermissions, AddWebAppEndpoints, AddWebAppAuthentication, AddWebAppAuthorization |
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) | host, updater, install store, session guard, bridge contracts, built-in settings and files endpoints, your own endpoints and their authentication; no MAUI dependency |
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 |
Shiny.AppDeviceBridge.Locations |
the app | AddGpsBridge(), AddGeofenceBridge(), AddLocationBridges(), AddMotionActivityBridge() |
Shiny.AppDeviceBridge.BluetoothLE |
the app | AddBluetoothLEBridge() |
Shiny.AppDeviceBridge.Obd |
the app | AddObdBridge(): OBD-II over Bluetooth LE or Wi-Fi adapters — decoded PIDs, VIN, trouble codes, live readings |
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.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.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.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, with each picked folder remembered as a file root across launches |
Shiny.AppDeviceBridge.TrayIcon |
the app | AddTrayIconBridge(): system tray / menu bar icons, menus, badges, notifications and animation, with clicks handed back to the web app |
The app
Section titled “The app”builder .UseMauiApp<App>() .UseWebAppHost(o => { o.AppId = "field-app"; o.UseBaseline(typeof(App).Assembly, "webapp.zip", "1.0.0"); // runs offline on first launch o.UpdateServer = new Uri("https://api.example.com/webapps"); o.PublicKey = """ -----BEGIN PUBLIC KEY----- ... -----END PUBLIC KEY----- """; }) .AddAppSupportBridge() .AddLocationBridges() .AddBluetoothLEBridge();public class App : Application{ protected override Window CreateWindow(IActivationState? state) => new(new WebAppHostPage());}Updates are optional. Without an UpdateServer nothing is checked, downloaded or signed, and the app
simply serves the zip compiled into it — which is a complete setup on its own:
o.AppId = "field-app";o.UseBaseline(typeof(App).Assembly, "webapp.zip"); // version defaults to 1.0.0There is no manifest, no signing key and no network at any point; the install directory is never even
created. Add UpdateServer and PublicKey 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 calls UseShiny() if nothing
has yet. 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.
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();

