Universal Hubs
Getting Started
Section titled “Getting Started”| GitHub | |
| Shared Protocol | |
| Host | |
| Client |
Universal Hubs serves BluetoothLE Hubs over Wi-Fi as well as Bluetooth LE. Write the hub once. Players on a plane connect over BLE. At home they connect over Wi-Fi, which has more range and bandwidth and no MTU. Both kinds of client share the same hub, groups and pushes.
The Wi-Fi side is a Shiny HTTP Server Switchboard (SSE + POST, with resume), advertised over mDNS.
The hub itself is a plain BluetoothLE Hubs hub: the same contract, source generated proxy, Clients, Groups and
IHubContext<THub>. Universal Hubs only carries the calls.
Features
Section titled “Features”- One hub, two transports.
Clients.All, groups,IHubContext<THub>,MaxClients,ValidateClientandContext.Abortcover BLE and Wi-Fi clients together. - One discovery list. BLE scanning and mDNS browsing are merged, so a host seen both ways shows up once.
- Wi-Fi first, BLE fallback. Connect prefers Wi-Fi and falls back to BLE. The generated proxy, events,
Disconnectedand file methods work the same on either transport. - Knows the transport.
UniversalHub<T>hooks get aHubConnectionthat says which transport the client is on. - Resume. A short Wi-Fi drop keeps the connection id and groups, and missed pushes are replayed. The hub sees no disconnect.
- One hub at a time. Run an always-on hub beside one that only runs on demand.
- Files: L2CAP over BLE, plain HTTP over Wi-Fi, behind the same
UploadFile/DownloadFile. - Tunnels. Hubs can opt in to being reachable from the internet.
- AOT and trim safe, with no reflection.
-
Install the packages. The host app needs
Host, the client app needsClient, and an app that can play either role installs both. The shared protocol package comes along automatically.Terminal window dotnet add package Shiny.UniversalHubs.Hostdotnet add package Shiny.UniversalHubs.Client -
Describe the hub in an interface both sides can see. It is a normal BluetoothLE Hubs contract:
using Shiny.BluetoothLE.Hubs;[BleHubClient]public interface IChatHub{Task<bool> Send(string message); // client -> hostevent Action<string, string> Received; // host -> clients (who, message)} -
Implement it on the host by deriving from
UniversalHub<TContract>:public class ChatHub : UniversalHub<IChatHub>{public async Task<bool> Send(string message){// this.Connection.Transport says whether the caller is on Ble or Httpawait this.Clients.All.Received(this.Context.Client.Name ?? "?", message);return true;}} -
Register everything in
MauiProgram. Each hub has a BLE characteristic UUID and an mDNS service type, and the client uses the same two:// host - BLE (GATT) and Wi-Fi (Switchboard + mDNS) at oncebuilder.Services.AddUniversalServer(server => server.ServiceUuid(ServiceUuid) // optional - the one BLE service; clients use the same.AddHub<ChatHub>(ChatCharacteristicUuid, "_mychat._tcp"));// clientbuilder.Services.AddUniversalHubClient<IChatHub>(ChatCharacteristicUuid, "_mychat._tcp", o => o.ServiceUuid = ServiceUuid); -
Start hosting on one device:
// IUniversalHubHost host - BLE, Wi-Fi or both, whichever this device can do right nowHubTransports running = await host.Start(); -
Discover, connect and call it from another:
// IUniversalHubClient<IChatHub> clientclient.Hub.Received += (who, message) => Messages.Add($"{who}: {message}");var host = await client.DiscoverAll().FirstAsync(x => x.Transports != HubTransports.None);await client.Connect(host, new HubConnectOptions("Allan")); // Wi-Fi when it can, BLE otherwiseawait client.Hub.Send($"hello over {client.Transport}!");
Platform Setup
Section titled “Platform Setup”iOS (Platforms/iOS/Info.plist). List every hub’s mDNS service type in NSBonjourServices, on both the host and
the client:
<key>NSBluetoothAlwaysUsageDescription</key><string>Bluetooth is used to talk to nearby devices</string><key>NSLocalNetworkUsageDescription</key><string>The local network is used to talk to nearby devices</string><key>NSBonjourServices</key><array> <string>_mychat._tcp</string></array><!-- clients: hubs on the local network are plain HTTP to a private address --><key>NSAppTransportSecurity</key><dict> <key>NSAllowsLocalNetworking</key> <true/></dict>Android (Platforms/Android/AndroidManifest.xml). The BLE permissions from
BluetoothLE Hubs, plus:
<uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- clients: hubs on the local network are plain HTTP --><application android:usesCleartextTraffic="true" ... />BLE stacks. On Android, iOS and Mac Catalyst the BLE hosting and client stacks are registered for you. On plain
net10.0 (Linux, tests, macOS AppKit), register an IBleHostingManager / IBleManager yourself.
| Feature | iOS | macOS / Mac Catalyst | Android |
|---|---|---|---|
| Host over BLE | Foreground | Yes | Yes |
| Host over Wi-Fi | Foreground | Yes | Yes |
| Connect as a client | Yes | Yes | Yes |
| File transfers | Yes | Yes | Wi-Fi, or L2CAP on API 29+ |
Things to Know
Section titled “Things to Know”- Plane, hotel and public Wi-Fi usually isolate clients from each other. mDNS and device-to-device traffic are blocked there, which is why BLE is the fallback.
- Hosting runs while the app is in the foreground, the same as BLE hosting.
- The host needs a BLE hosting stack registered even if Bluetooth is off. Wi-Fi-only hosting works on a device whose Bluetooth is off or unavailable.
- Hub values travel as JSON over Wi-Fi. They are re-framed, never re-serialized, so
IBleHubSerializermust produce JSON (the default does). - An mDNS TXT record fits about six hubs per host.
Sample
Section titled “Sample”The sample is the BluetoothLE Hubs tic-tac-toe game hosting over both transports, plus an always-on chat, for iOS, Android and macOS:
- Each hosted game runs only while hosting (
StartHub/StopHub<GameHub>). The join list shows how each game is reachable, and the game screen says which transport is in use. - The chat hub starts with the app and stays open. “Share on the internet” starts a quick tunnel, which only the chat hub allows.
- Hosting:
AddUniversalServer,UniversalHub<T>, start/stop, tunnels, disconnect reasons - Connecting Clients: discovery, transport choice, resume, connecting by address, files
- How It Works: the Wi-Fi wire protocol, mDNS discovery and file routes
Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install the plugin:
claude plugin install shiny@shinyOne plugin installs all 48 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 48 Shiny skills. Your agent loads only the skill relevant to what you're building, so there's no cost to having them all available.


