This document specifies onbraid (onbraid.app) — the first modnet browser. onbraid is the native client application that connects the user to their sovereign agent node and to any other nodes they discover or join. The app is a thin client: nodes run remotely, generate all UI via the controller protocol (render/attrs/update_behavioral), and stream it over WebSocket. The app manages multiple simultaneous node connections, handles authentication per node, provides platform-native affordances (gestures, notifications, QR scanning, proximity discovery, sharing), and renders server-generated HTML.
The app can connect to multiple nodes at once — the user's own sovereign node (always primary), enterprise/work nodes, ephemeral local nodes (farmer's market, conference, library), and service nodes. The user's sovereign PM is always the authority — even when browsing another node's content, the user's agent mediates outbound A2A decisions.
Plaited is the open-source framework — the TypeScript/Bun runtime, the controller protocol, the behavioral engine, the A2A networking layer, the server infrastructure. Plaited is what developers use to build and deploy sovereign agent nodes.
onbraid is the consumer-facing product — the native app people install on their phone, tablet, or desktop to interact with the modnet. onbraid is to Plaited what Chrome is to Chromium, what Safari is to WebKit. The framework is infrastructure; onbraid is the experience.
onbraid is the first modnet browser. Others could be built — the controller protocol and Agent Card discovery are open standards. But onbraid is the reference implementation and the product at onbraid.app.
The metaphor is HyperCard's Home Stack reimagined for a connected world: a personal hub for navigating your computational universe, but instead of local stacks on a single machine, it's a portal to a network of sovereign agents — yours at the center, others joined by choice.
This spec captures interaction patterns, navigation models, and platform-native features that onbraid must support. It is intended as seed material for generating native iOS (Swift/SwiftUI), Android (Kotlin/Compose), and desktop (PWA + Mac Catalyst) implementations.
onbraid is a modnet browser — it can connect to multiple sovereign nodes simultaneously, just as a web browser can have multiple tabs open to different servers. The user's own sovereign node is the primary connection, but the app also maintains connections to:
- The user's sovereign node — their personal agent, always connected when online
- Enterprise/work nodes — an employer's node the user authenticates to via OIDC/SSO
- Ephemeral network nodes — a farmer's market node, a conference node, a library node — joined temporarily via QR scan, proximity, or explicit URL
- Service nodes — nodes offering paid services the user subscribes to (a streaming provider, a professional directory)
Each node connection is an independent WebSocket with its own session. The app manages multiple WebViews or a single WebView that switches between node contexts. The user's PM on their sovereign node is always the authority for outbound A2A — even when browsing another node's content, the user's agent mediates.
App manages a connection registry:
┌─ Own sovereign node (always-on, primary)
│ WebSocket 1 → wss://my-node.example.com/ws
│ Auth: passkey
│ Role: home, personal modules, PM authority
│
├─ Work node (persistent, secondary)
│ WebSocket 2 → wss://acme-corp.example.com/ws
│ Auth: OIDC/SSO
│ Role: work modules, enterprise context
│
├─ Farmer's market node (ephemeral)
│ WebSocket 3 → wss://hillsdale-market.local/ws
│ Auth: none (boundary=all) or token from QR
│ Role: browse vendors, purchase
│ Lifecycle: connected on QR scan, disconnected on leave
│
└─ Library node (ephemeral)
WebSocket 4 → wss://multnomah-library.local/ws
Auth: library card number
Role: catalog browse, reading list sync
Lifecycle: connected on proximity, disconnected on leave
The app's home surface shows all connected nodes. The user's sovereign node generates the home surface — it's aware of all connections because it's the PM that authorized them. Tapping a node switches the active WebView context to that node's card stack.
Multi-device: multiple devices can connect to the same sovereign node simultaneously. Each device also independently manages its own ephemeral connections (your phone at the market has the market node connected; your laptop at home does not).
Discovery (how you find a node):
QR scan → extract Agent Card URL
BLE beacon → receive Agent Card URL
mDNS/Bonjour → discover on local wifi
NFC tap → exchange Agent Card URLs
Geofence trigger → agent suggests known nearby nodes
Manual URL entry → type or paste
Deep link → from email, message, or another app
Evaluation (your PM decides):
App fetches Agent Card at discovered URL
App forwards Agent Card to your sovereign node's PM
PM evaluates: constitution rules, boundary compatibility, trust level
PM presents decision to user if needed (boundary=ask)
PM may auto-connect for known/trusted nodes
Connection:
App opens WebSocket to the new node
Auth handshake (varies: passkey, OIDC, token, none)
Node streams initial card via controller protocol
Connection added to app's registry
Disconnection:
User toggles off in network panel
Geofence exit triggers auto-disconnect
Session timeout (ephemeral nodes)
User's PM revokes connection
App closes WebSocket, removes from registry
The fundamental UI unit in onbraid is the card — a full-screen or near-full-screen rendered view that represents a single context: a module, a network view, a conversation, a form, a dashboard. This directly echoes HyperCard's card-based navigation where each card was a discrete interactive screen, not a scrollable document.
Cards are generated by the server — they arrive as render messages targeting the app's root p-target. The app doesn't know what's on a card; it just renders HTML and handles gestures.
Cards organize into stacks — ordered sequences the user navigates through. A stack represents a single module or context at a single scale level. Navigation within a stack is horizontal (left/right swipe). Navigation between stacks is vertical (up/down or modal presentation).
Stack: My Farm Stand (S5)
├── Card 1: Dashboard (overview)
├── Card 2: Produce inventory
├── Card 3: Today's sales
└── Card 4: Pending inquiries
Stack: Hillsdale Market (S6)
├── Card 1: Market overview (all vendors)
├── Card 2: My stand in context
└── Card 3: Market stats
The server controls stack composition — it decides which cards exist and in what order. The app provides the gesture-based navigation between them and sends user_action: { type: 'navigate', direction: 'next' | 'prev' | 'up' | 'down' } to the server, which responds with the appropriate render message.
The HyperCard-inspired interaction that makes modnet navigation distinctive: pinch gestures navigate the scale hierarchy. Pinch out (spread fingers) to zoom from a detail view (S1 item) to the containing collection (S3), to the module (S5), to the network (S6). Pinch in (pinch fingers) to drill into a specific item.
Pinch out (zoom out):
S1 (single apple listing)
→ S2 (produce list)
→ S3 (categorized collections)
→ S5 (farm stand module)
→ S6 (market overview)
Pinch in (zoom in):
S6 (market overview)
→ S5 (specific vendor stand)
→ S3 (vendor's produce categories)
→ S2 (specific category list)
→ S1 (individual item detail)
The app translates pinch gestures into scale-navigation user_action messages:
{ "type": "user_action", "detail": { "action": "scale_navigate", "direction": "out" } }
{ "type": "user_action", "detail": { "action": "scale_navigate", "direction": "in", "target": "produce-category-fruits" } }The server handles the scale transition — it generates the appropriate view for the new scale level and sends a render message with a transition hint (animation direction).
The app provides smooth transitions between cards and scale levels:
| Gesture | Navigation | Animation |
|---|---|---|
| Swipe left | Next card in stack | Slide left |
| Swipe right | Previous card in stack | Slide right |
| Pinch out | Zoom to parent scale | Zoom-out with crossfade |
| Pinch in / tap item | Zoom to child scale | Zoom-in to tapped element |
| Swipe down from top | Dismiss current stack / return to home | Slide down |
| Long press | Context menu (actions for this card) | Popup from press point |
The server can include transition hints in render messages:
{ "type": "render", "detail": { "target": "main", "html": "...", "transition": "scale-out" } }The app uses the hint to choose the right animation. If no hint, defaults to instant swap.
The app's root view — generated by the user's sovereign node. This is the user's personal home, showing their modules AND their active connections to other nodes. It's the S7/S8 view of the user's entire modnet presence.
Layout: The sovereign node generates this as a card. It shows:
- Personal modules as a grid/mosaic (knowledge garden, health tracker, recipe collection, etc.)
- Connected nodes as a separate section or row (work node, market node, library node) — each showing a thumbnail and live status
- Recent activity stream across all connections
Interactions:
- Tap a personal module → opens its card stack within the sovereign node (zoom-in animation)
- Tap a connected node → switches to that node's card stack via the node switcher
- Long press a module → context menu (settings, disconnect, share, archive)
- Long press a connected node → context menu (disconnect, view Agent Card, manage boundary)
- Pull down → refresh / agent conversation input
- Floating action button → quick-create new module, scan QR to connect to a node, or talk to agent
Server generates this: The home surface is a render message from the sovereign node. The node knows about all connections because the PM authorized them. The app doesn't hardcode any layout — the sovereign node composes it based on the user's modules, connections, and preferences.
The primary way the user communicates intent to their agent. This is NOT a separate chat app — it's an overlay or drawer accessible from any card.
Layout: A conversational interface (message bubbles or similar) anchored to the bottom of the screen, pullable from any context. The agent's responses can include inline rendered cards (mini-previews of modules the agent is creating or modifying).
Interactions:
- Pull up from bottom → opens conversation drawer
- Type or voice → sends intent to agent
- Agent responds with text + optionally rendered UI previews
- Tap a preview → opens that module's card stack
- "Show me..." requests → agent navigates directly to the relevant card
Key principle: The agent conversation is contextual. If the user opens the conversation while viewing their farm stand, the agent knows that context. "Add a new item" means "add to this farm stand," not "create a new module."
Shows all node connections and network participation — the full picture of the user's modnet presence.
Layout: Accessible via swipe from right edge or a persistent tab. Two sections:
Connected Nodes:
- Each node shows: name, type icon (sovereign/enterprise/ephemeral/service), connection status, auth method
- Sovereign node always at top, marked as primary
- Enterprise nodes show org badge
- Ephemeral nodes show discovery method (QR, BLE, wifi) and time connected
Network Participation (per node):
- Which modules on this node are participating in which networks
- Toggle to connect/disconnect individual modules from specific networks
- Connection duration and peer count
Interactions:
- Tap a node → switches main view to that node's card stack
- Swipe left on an ephemeral node → disconnect with confirmation
- Toggle module participation → connect/disconnect that module from a network
- Long press a node → shows Agent Card details, boundary settings, auth info
- "Scan QR" button at bottom → opens scanner for new node connections
How the user finds and connects to new nodes. This is the modnet equivalent of typing a URL into a browser — but with multiple discovery channels beyond manual entry.
Layout: Accessible via a dedicated tab or from the home surface. Shows:
- Connected nodes — all active connections with status indicators
- Nearby — nodes detected via proximity (BLE, wifi, geolocation)
- Recent — previously connected nodes (history)
- Scan — QR/NFC scanner always accessible
Discovery Scenarios by Environment:
Farmer's market (outdoor, no shared infrastructure):
The market organizer prints a QR code on a sign at the entrance. The QR encodes the market node's Agent Card well-known URL (e.g., https://hillsdale-market.example.com/.well-known/agent.json). The shopper scans the QR, their app fetches the Agent Card, their PM evaluates it, and on approval the app opens a WebSocket to the market node. Geolocation acts as the participation boundary — the market node validates that connecting clients are within the market's defined geographic area. If you scan the QR from home, the market node rejects the connection because you're outside the geofence. This combo (QR for discovery + geolocation for participation boundary) works with zero infrastructure — no wifi, no bluetooth, just a printed sign and GPS.
Coffee shop (shared wifi, casual): The shop runs a node on the local network. The app discovers it via mDNS/Bonjour — no scanning needed. When you join the shop's wifi, the app detects the node and shows it in the Nearby section: "Stumptown Coffee — Community Board." Tap to connect. The wifi network itself is the participation boundary. Leave the wifi, connection drops.
Conference (BLE beacons, semi-structured): Conference organizers place BLE beacons at registration, session rooms, and the expo hall. Each beacon advertises a different node's Agent Card URL — the main conference node, per-session discussion nodes, vendor booth nodes. The app passively detects beacons and populates the Nearby section. Tap to connect to the ones you're interested in. BLE signal strength serves as proximity — you only see the session room node when you're in or near the room.
Person-to-person (NFC tap or QR exchange): Two people meet and want to connect their nodes. One shows a QR code (generated on-demand by their agent — a time-limited URL to their Agent Card), the other scans it. Or they tap phones (NFC exchanges the Agent Card URL). This is the modnet equivalent of exchanging business cards. The PM on each side evaluates the other's Agent Card and decides what to expose (boundary=ask → user confirms what to share).
Enterprise (managed registry): The employee's work node is pre-configured. IT provisions the connection via a deep link or MDM profile. The app auto-connects to the enterprise node on launch using OIDC/SSO. No discovery needed — the connection is permanent and managed.
Remote/internet (manual or link): Someone sends you a link to their node's Agent Card (in an email, a Bluesky post, a message). Tapping the link opens onbraid, which fetches the Agent Card and initiates the connection flow. This is the fallback for any scenario where proximity doesn't apply.
Interactions:
- Tap a discovered node → preview card showing the node's Agent Card (name, capabilities, what connecting would expose)
- "Connect" button → your PM negotiates, you confirm if boundary=ask
- Geofence trigger → auto-discovery, but never auto-connect without PM evaluation
- QR scanner → always accessible from the discovery surface and as a floating button overlay
These patterns emerged from mapping the five Modnet usage scenarios to mobile-first interaction design. The server generates all the HTML — these describe the gestures and navigation the app must support.
Context: User maintains notes, bookmarks, reading lists with selective sharing.
Mobile interactions:
- Card-based interface where each note is a card in a stack
- Swipe left/right navigates between notes within a collection
- Pinch out zooms from individual note (S1) → collection (S3) → full knowledge garden (S5) → shared research group (S6)
- Floating action button creates new notes — the app sends
user_action: { action: 'create', context: 'current-collection' }and the server generates an editor card - Push notifications for collaboration requests: "Dr. Patel wants to access your 'Attention Mechanisms' collection"
- Notification tap deep-links to the sharing approval card
Context: Vendor at a market, shoppers browsing nearby stands.
Vendor mobile interactions:
- Card deck view — each product is a card with photo, price, description
- Swipe to navigate products, long press to edit
- "Go Live" button activates the stall module and connects to nearby market networks
- Live dashboard card shows real-time stats: view count, message inbox, inventory toggles
- Tap "Go Live" again to disconnect (ephemeral lifecycle)
Shopper mobile interactions:
- Map view showing active stalls with distance indicators and category icons
- Tap a stall pin → opens the vendor's product collection as a scrollable card sequence
- Filter bar at top → filters by category (produce, crafts, food trucks)
- "Contact vendor" button → triggers A2A message through the PM
Context: Freelancer managing professional identity across multiple professional networks.
Mobile interactions:
- Multi-card stack where main card shows public profile
- Swipe reveals successive cards: portfolio, testimonials, availability, rate sheet
- Network panel shows active connections (conference network, two client discovery networks) with toggle controls
- Push notifications surface high-priority inquiries: "New client inquiry from AIGA conference"
- Quick-action from notification: "Reply" / "View profile" / "Decline"
Context: Chronic condition tracking with privacy-first sharing.
Mobile interactions:
- Home card shows daily wellness dashboard with quick-log buttons (symptoms, medications, exercise)
- Tap a quick-log button → small input card slides up (not a full-screen transition — a sheet)
- Swipe between dashboard views: daily → weekly → monthly trends
- Long-press on the dashboard → privacy panel appears showing all active sharing sessions with clear revocation buttons
- Peer group view as a separate card stack — anonymized community metrics alongside personal progress
- Sharing approval as a blocking card: "Dr. Chen's office is requesting medication history. Approve for this session?"
Context: Musicians co-creating across distributed nodes.
Mobile interactions:
- Multi-track timeline card with each track color-coded by contributor
- Horizontal scroll navigates the timeline; vertical scroll switches between tracks
- Tap a track → detail card with version history, comments, waveform preview
- Collaboration panel (swipe from right) shows other musicians' online status, pending proposals, voting queue
- Proposal notification card: "Alex proposed moving the bridge to bar 32. [Listen] [Accept] [Counter-propose] [Block]"
- Drag-and-drop reordering on the arrangement card with real-time sync across all connected nodes
| Gesture | Action | user_action Message |
|---|---|---|
| Swipe left | Next card in stack | { action: 'navigate', direction: 'next' } |
| Swipe right | Previous card in stack | { action: 'navigate', direction: 'prev' } |
| Swipe down from top | Return to parent / home | { action: 'navigate', direction: 'up' } |
| Pinch out | Scale zoom out (S1→S2→...→S6) | { action: 'scale_navigate', direction: 'out' } |
| Pinch in | Scale zoom in | { action: 'scale_navigate', direction: 'in' } |
| Tap | Select / activate | Standard p-trigger handling |
| Long press | Context menu | { action: 'context_menu', target: '<element-id>' } |
| Pull down (on content) | Refresh current card | { action: 'refresh' } |
| Pull up from bottom | Open agent conversation | App-level, no server message |
| Swipe from right edge | Open network panel | App-level, no server message |
Three categories, matching the Variant 3 progressive enhancement pattern:
- Wake-up only — generic "You have something to look at." Tap opens app, WebSocket reconnects, full UI resumes.
- Rich notification — server provides title, body, optional action buttons. Tap deep-links to specific card.
- Actionable notification — 2-3 action buttons that send
user_actionmessages directly without opening the app (approve/deny sharing request, accept/reject proposal).
Platform mapping:
- iOS: APNs with UserNotifications framework
- Android: FCM with NotificationCompat
- Desktop PWA: Web Push API with service worker
The app maintains continuous awareness of the local environment and surfaces discovery events to the user's sovereign node for PM evaluation. Discovery is passive — the app never auto-connects without PM approval.
Discovery channels and what they carry:
| Channel | What the app detects | What it extracts | Participation boundary |
|---|---|---|---|
| QR code scan | User points camera at a printed/displayed QR | Agent Card well-known URL | Geolocation (market node validates GPS) |
| BLE beacon | Passive scan detects advertising beacons | Agent Card URL in beacon payload | Signal strength / proximity |
| mDNS/Bonjour | Node on same local network advertising | Agent Card URL via DNS-SD service record | Network membership (same wifi) |
| NFC tap | Physical tap between two devices | Agent Card URL via NDEF record | Physical contact (implicitly trusted) |
| Geofence | GPS enters a predefined region | Agent suggests known nodes for that region | GPS coordinates |
| Wifi captive portal | Joining a wifi network that has a modnet node | Agent Card URL in captive portal metadata | Network membership |
The QR code is the Agent Card well-known URL. When you scan a QR code at a farmer's market, you're reading a URL like https://hillsdale-market.example.com/.well-known/agent.json. The app fetches the Agent Card at that URL. The Agent Card declares the node's capabilities, supported auth schemes, and boundary requirements. Your PM evaluates the card and decides whether to connect.
Geolocation as participation boundary. For ephemeral location-based nodes (markets, events, parks), the node can require clients to be within a geographic boundary. The app sends geolocation with the connection request:
{
"type": "connection_request",
"agentCardUrl": "https://hillsdale-market.example.com/.well-known/agent.json",
"discoveryChannel": "qr",
"geolocation": { "lat": 45.4831, "lng": -122.7765, "accuracy": 10 }
}The market node's constitution includes a geofence rule — it rejects connections from outside the defined area. This means you can scan the QR code from anywhere (it's just a URL), but you can only participate when you're physically present.
Search vs. participation boundaries (Jaffe's distinction):
- Search boundary — how far away you can discover a node. QR codes have infinite search boundary (anyone with the URL can see the Agent Card). BLE has ~30m. Wifi has the network range.
- Participation boundary — how close you must be to actually connect and interact. Geofencing enforces this independently of discovery channel. A market node might have a 200m participation boundary regardless of whether you discovered it via QR, BLE, or a friend's link.
Discovery events sent to the user's sovereign node:
{ "type": "user_action", "detail": {
"action": "nodes_discovered",
"nodes": [
{
"agentCardUrl": "https://hillsdale-market.example.com/.well-known/agent.json",
"channel": "qr",
"geolocation": { "lat": 45.4831, "lng": -122.7765 },
"timestamp": "2026-03-21T10:15:00Z"
},
{
"agentCardUrl": "https://stumptown-coffee.local/.well-known/agent.json",
"channel": "mdns",
"networkName": "Stumptown-Guest",
"timestamp": "2026-03-21T10:14:30Z"
}
]
} }The user's PM fetches each Agent Card, evaluates it against the constitution, and decides what to present to the user. Known/trusted nodes may auto-connect. Unknown nodes get a preview card the user can accept or dismiss.
The app registers as both a share source and share target:
Share out: When the user wants to share a module artifact externally (not via A2A — via the platform's share sheet to non-modnet apps). The server generates a shareable representation (link, image, text) and the app invokes the native share sheet.
Share in: When content comes in from another app (a photo from the camera, a URL from the browser, a file from email), the app receives it and sends it to the server:
{ "type": "user_action", "detail": { "action": "share_received", "contentType": "image/jpeg", "uri": "content://..." } }The server's PM decides which module should handle the incoming content.
The app provides haptic feedback for key interactions:
- Light tap for card navigation
- Medium impact for scale transitions
- Success/error haptics for share approval/denial, network connect/disconnect
- Continuous feedback during drag-and-drop reordering
The server can request haptics via protocol:
{ "type": "haptic", "detail": { "style": "impact", "intensity": "medium" } }The app is a thin client — without a WebSocket connection, it has limited functionality. But it should handle disconnection gracefully:
- Show last-rendered card with a "reconnecting..." indicator
- Queue user actions (taps, form inputs) and replay on reconnect
- Allow reading cached cards (the WebView's in-memory state persists)
- Push notification tap should attempt reconnection automatically
- If the server is unreachable for extended periods, show a "Your agent is offline" card with connection diagnostics
On devices that support it, the app should allow multiple card stacks side by side:
- Tablet: split view with two card stacks (e.g., reading list on left, library browser on right)
- Desktop: resizable windows, each connected to the same WebSocket session
- The server tracks which card stack is rendered in which viewport via the pub/sub topic system (sessionId:island-tag-name)
The app should respect the server-generated theme. The server sends CSS custom properties as part of the initial render, and the app's WebView inherits them. The app shell (native chrome around the WebView) should adapt to match:
- Status bar color matches the current card's theme
- Navigation gesture overlays are semi-transparent and adapt to light/dark
- System UI elements (share sheet, notification cards) use the same color palette where possible
The server controls theming — the app follows. Theme changes arrive as attrs messages updating CSS custom properties on the root element.
- App launches → native passkey prompt (Face ID, fingerprint, PIN)
- Passkey verified → app loads sovereign node URL in WebView
- WebView receives session cookie (HttpOnly, SameSite=Strict)
- WebView establishes WebSocket using the session cookie
- Server validates session → streams home surface showing all connections
For first-time setup: the user needs their sovereign node URL. This can be entered manually, scanned via QR code from the node's provisioning output, or received via a setup deep link.
- User taps "Add Node" in the connection manager or follows a deep link
- App fetches the Agent Card at the target URL
- Agent Card declares its auth scheme (OIDC, API key, OAuth 2.0, passkey)
- App presents the appropriate auth flow:
- OIDC/SSO → native browser redirect → token exchange → session established
- Passkey → WebAuthn ceremony with the target node
- API key → user enters or pastes key
- OAuth 2.0 → authorization flow with consent screen
- On success, WebSocket opens to the new node
- Connection saved to app's registry for reconnection on future launches
- User scans QR code / detects BLE beacon / enters wifi network
- App extracts Agent Card URL from the discovery channel
- App forwards Agent Card to user's sovereign PM for evaluation
- PM approves → app opens WebSocket to the ephemeral node
- Auth varies:
boundary: allnodes → no auth required, anonymous sessionboundary: asknodes → the node presents a consent card, user confirms- Token-from-QR → QR payload includes a time-limited connection token
- Connection is NOT saved to registry — it's ephemeral by definition
- Disconnection happens automatically on geofence exit, wifi leave, or user toggle
The app maintains a persistent registry of saved nodes:
Registry:
├─ sovereign: { url, auth: 'passkey', autoConnect: true }
├─ work: { url, auth: 'oidc', autoConnect: true, issuer: '...' }
├─ streaming-service: { url, auth: 'oauth', autoConnect: false }
└─ (ephemeral connections are NOT persisted)
On app launch: auto-connect to all nodes with autoConnect: true. Others connect on user action. Ephemeral connections are established fresh each time via discovery.
The app shell is the native frame around the WebView. It is minimal and should not duplicate server functionality.
- Status bar — system time, battery, signal (standard OS)
- Node indicator — shows which node is currently active (sovereign node icon, enterprise badge, ephemeral node name). Tap to open the node switcher.
- Agent pull-up handle — a small drag handle at the bottom edge, always accessible, opens the conversation drawer with the user's sovereign agent (even when viewing another node's content)
- Connection status — subtle indicator per active node (connected/reconnecting/offline), visible in node switcher
Accessible by tapping the node indicator or swiping down from the top:
- Shows all connected nodes as a vertical list or card stack
- Each node shows: name, icon, connection type (sovereign/enterprise/ephemeral), status (connected/disconnecting)
- Tap a node → switches the main WebView to that node's card stack
- Swipe left on an ephemeral node → disconnect
- "Scan QR" button always visible at the bottom
- "Add Node" button for manual URL entry
The node switcher is the modnet equivalent of a browser's tab bar. It's onbraid's primary native UI surface. It's native UI, not server-rendered, because it manages connections across multiple servers.
- Back gesture zone — left edge swipe zone for stack navigation (iOS) or system back (Android)
- Scale gesture zone — pinch anywhere on the card area
- Notification banner — slides down from top for real-time notifications from any connected node (tagged with which node it's from)
- QR scanner overlay — accessible from the node switcher and as a floating button, always one tap away
Everything else is server-rendered HTML inside the WebView. The active node's server controls what's rendered. The app does not render any UI above what the server sends, except for the persistent and conditional elements listed above.
When the user switches nodes in the node switcher, the app either:
- Swaps the WebView's URL to the new node's server (destroying the previous render)
- Maintains multiple WebViews in a stack and brings the target to front (preserving state for quick switching)
The second approach is preferred for frequently-used nodes (sovereign + enterprise) and the first for ephemeral connections where state persistence doesn't matter.
| Platform | Technology | Covers | Rationale |
|---|---|---|---|
| iPhone + iPad + Mac | Swift / SwiftUI + WKWebView | All Apple devices | SwiftUI's native iPad multitasking (split view, Stage Manager, slide over) makes multi-node split view first-class. Mac Catalyst gives desktop for free. |
| Android | Kotlin / Jetpack Compose + Android WebView | Android phones + tablets | Native gesture APIs, AndroidX credentials for passkeys, FCM for push. |
| Desktop (Windows/Linux) | PWA | Non-Apple desktops | The server does all rendering. Desktop browsers support WebSocket, WebAuthn, and everything except BLE/NFC. PWA is sufficient until native is justified. |
Tauri was evaluated and rejected due to CSP conflicts with dynamic code loading (update_behavioral requires import(url) which Tauri's CSP blocks). React Native and Flutter both add an abstraction layer over the WebView that conflicts with the thin-client principle — the app IS a WebView with native chrome, not a native UI rendering server content. KMP was considered for shared connection logic but the toolchain complexity isn't justified when the native layer is this thin.
The app is thin enough that two native codebases (Swift + Kotlin) are manageable. Both implement the same spec. The complex parts are identical across platforms: WebSocket connection registry, Agent Card parsing, gesture-to-user_action translation, discovery pipeline. The platform-specific parts are well-contained: WKWebView vs Android WebView, APNs vs FCM, AVCaptureSession vs CameraX for QR, CoreBluetooth vs Android BLE, CoreLocation vs Android Location.
Pinch-to-zoom gesture interception. Both WKWebView and Android WebView have built-in pinch-to-zoom behavior. The app MUST disable the WebView's default zoom and replace it with the custom scale-navigation gesture. On iOS: set WKWebViewConfiguration appropriately and use UIGestureRecognizer to intercept pinch before the WebView. On Android: set WebSettings.setBuiltInZoomControls(false) and use ScaleGestureDetector. The gesture handler translates pinch into user_action: scale_navigate messages, not viewport zoom.
WebView security. The WebView loads content from authenticated server URLs only. No arbitrary URL navigation. JavaScript bridge is one-way: the WebView can send user_action messages to the native layer (for gestures and platform events), and the native layer can inject discovery events into the WebView. The native layer never executes arbitrary JavaScript from the server — all UI updates flow through the controller protocol inside the WebView.
Multiple WebView management. For multi-node, the app maintains a pool of WebViews — one per connected node. Frequently-used nodes (sovereign, enterprise) keep their WebView alive in the background for instant switching. Ephemeral nodes get a WebView on connect that's destroyed on disconnect. iPad split view shows two WebViews side by side from different nodes.
This spec follows the autoresearch program+slice approach used by other Plaited development lanes. The onbraid program lives at dev-research/browser-app/.
dev-research/browser-app/
program.md — program overview (references this spec)
slice-1-mvp.md — Phase 1: Minimum Viable Browser
slice-2-multi.md — Phase 2: Multi-node + card navigation
slice-3-discover.md — Phase 3: Discovery + platform integration
slice-4-polish.md — Phase 4: Enterprise + polish
This program is distinct from:
dev-research/native-model/program.md— Falcon model training (produces the agent intelligence)dev-research/modnet/program.md— inter-node A2A design (defines Agent Card schema, A2A protocol)dev-research/runtime-taxonomy/program.md— framework infrastructure (controller protocol, createServer, pub/sub)
onbraid consumes the generative UI controller protocol from the framework. It consumes A2A Agent Cards for discovery. It does NOT implement A2A directly — the sovereign node handles all A2A via the PM. onbraid is a rendering and discovery surface, not an agent.
Goal: A working thin client that connects to one sovereign node, authenticates via passkey, renders server-generated cards, and supports basic interaction.
Scope:
- WebView loading a single server URL
- Passkey authentication for sovereign node
- WebSocket lifecycle (connect, reconnect, offline handling)
- Basic card rendering via controller protocol
- Pull-up agent conversation drawer
- Push notifications (wake-up only)
- QR code scanner (camera-based, extracts URL)
iOS implementation:
- SwiftUI app with
WKWebViewas the primary content view (UIViewRepresentablewrapper) - WebAuthn passkey flow using
ASAuthorizationController(AuthenticationServices framework) WKWebViewloads the sovereign node URL after passkey verification- WebSocket lifecycle: the WebView establishes its own WebSocket to the server via the controller protocol JavaScript — the native layer monitors connection state via
window.webkit.messageHandlers.connectionState.postMessage(state)bridge - QR scanner using
AVCaptureSessionwithAVMetadataObjectTypeQRCode— extracts URL, displays it for user confirmation - Agent conversation drawer: native bottom sheet containing an overlay that the sovereign node renders into via a dedicated
p-target - Push: register for APNs, send device token to sovereign node via
user_action, handle wake-up notifications that open the app and reconnect
Android implementation:
- Kotlin/Jetpack Compose app with
AndroidViewhostingWebView - WebAuthn passkey flow using AndroidX Credential Manager (
CredentialManagerAPI) WebViewloads the sovereign node URL after passkey verification- WebSocket lifecycle: same pattern — WebView handles WebSocket internally, native layer monitors via
WebView.addJavascriptInterfacefor connection state callbacks - QR scanner using CameraX with ML Kit BarcodeScanning
- Agent conversation drawer: Compose
BottomSheetScaffoldhosting overlay WebView - Push: FCM registration, device token sent to sovereign node, wake-up handling
Acceptance criteria:
- App authenticates to a running Plaited server node via passkey
- Server-generated UI renders correctly in the WebView
- Tapping
p-triggerelements sendsuser_actionmessages and receivesrender/attrsresponses - QR scanner extracts a URL from a QR code and displays it
- Push notification wakes the app and reconnects the WebSocket
- Disconnection shows a reconnecting indicator; reconnection restores the last-rendered card
Not in scope: Multi-node, custom gesture interception, BLE/NFC/mDNS discovery, offline caching, OIDC/SSO.
Goal: The app manages multiple simultaneous node connections and implements the card/stack/scale navigation model.
Scope:
- Node connection registry (add/remove/switch nodes)
- Node switcher UI (native)
- Multiple simultaneous WebSocket connections
- Swipe left/right for card stack navigation
- Pinch zoom for scale navigation (with WebView default zoom disabled)
- Transition animations between cards and scale levels
- Network panel (swipe from right)
- Rich push notifications with deep linking and node tagging
- iPad split view with two WebView panes
iOS implementation:
- Node registry:
UserDefaultsor small SwiftData store persisting{ url, authType, autoConnect, label }per saved node - Node switcher: SwiftUI sheet presenting all connected nodes with status indicators
- Multiple
WKWebViewinstances in a pool — sovereign always alive, ephemeral created/destroyed on connect/disconnect - Gesture interception:
- Disable
WKWebViewdefault pinch zoom via viewport meta injection and configuration UIPinchGestureRecognizeron WebView's superview — intercepts pinch, translates touser_action: { action: 'scale_navigate', direction: 'in'|'out' }injected viaevaluateJavaScriptUISwipeGestureRecognizerfor left/right card navigation- Coordinate with WebView gesture recognizers using
gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:)
- Disable
- iPad:
UISplitViewControlleror SwiftUINavigationSplitViewwith two WebView panes from different nodes - Transition animations: UIView transitions triggered by native layer, WebView content swaps mid-transition
Android implementation:
- Node registry: Room database or DataStore
- Node switcher: Compose navigation drawer or bottom sheet
- Multiple
WebViewinstances in a managed stack - Gesture interception:
webSettings.builtInZoomControls = false; webSettings.setSupportZoom(false)ScaleGestureDetectorfor pinch →scale_navigatemessages- Swipe detection via Compose gesture modifiers
- Tablet:
SlidingPaneLayoutor Compose adaptive layout with two WebView panes - Transition animations: Compose
AnimatedContentwrapping WebView container
Acceptance criteria:
- User can add a second node via URL entry or QR scan
- Node switcher shows all connected nodes with live status
- Tapping a node switches the active WebView
- Pinch gesture triggers scale navigation (server receives
scale_navigate, responds with card at new scale) - Swipe left/right navigates between cards in a stack
- iPad split view shows two nodes side by side
- WebView default zoom is fully disabled — pinch ONLY triggers scale navigation
- Push notifications are tagged with which node they came from
Not in scope: BLE/NFC/mDNS/geofence discovery, enterprise auth, offline caching.
Goal: Full discovery pipeline — all six channels — plus platform integration features.
Scope:
- QR scan → Agent Card fetch → PM evaluation → connection flow (refinement from slice 1)
- Geolocation as participation boundary (send coords with connection request)
- BLE beacon passive scanning
- mDNS/Bonjour discovery on local wifi
- NFC tap for Agent Card exchange
- Geofence-triggered auto-discovery
- Share sheet (in and out)
- Haptics
- Actionable notifications
Discovery pipeline (both platforms):
Discovery event (any channel)
→ App extracts Agent Card well-known URL
→ App fetches Agent Card JSON from URL
→ App sends Agent Card to sovereign node's PM via WebSocket:
{ type: 'user_action', detail: {
action: 'node_discovered',
agentCard: { ...parsed card... },
channel: 'qr|ble|mdns|nfc|geofence|manual',
geolocation: { lat, lng, accuracy }
}}
→ Sovereign PM evaluates card against constitution
→ PM responds with either:
- Auto-connect (trusted/known) → app opens WebSocket to new node
- Present for approval → app shows preview card, user confirms
- Reject → app shows brief dismissible notice
iOS implementation:
- BLE:
CBCentralManagerscanning for modnet service UUID in advertisement data - mDNS:
NWBrowser(Network framework) discovering_modnet._tcpservices - NFC:
NFCNDEFReaderSessionreading NDEF records containing Agent Card URLs - Geofencing:
CLLocationManagerwithCLCircularRegionmonitoring - Geolocation:
CLLocationManager.requestLocation()→ coords in connection request payload - Share sheet:
UIActivityViewControllerfor share-out, share extension for share-in - Haptics:
UIImpactFeedbackGenerator/UINotificationFeedbackGenerator/UISelectionFeedbackGeneratormapped to serverhapticprotocol messages
Android implementation:
- BLE:
BluetoothLeScannerwithScanFilterfor modnet service UUID - mDNS:
NsdManagerdiscovering_modnet._tcpservices - NFC:
NfcAdapterwith NDEF foreground dispatch - Geofencing:
GeofencingClientfrom Google Play services - Geolocation:
FusedLocationProviderClient→ coords in connection request - Share sheet:
Intent.ACTION_SENDfor share-out, intent filter for share-in - Haptics:
VibrationEffectmapped to serverhapticmessages
Acceptance criteria:
- Each discovery channel successfully extracts an Agent Card URL and triggers the pipeline
- Geolocation is included in connection requests; a test node with geofence rejects out-of-area connections
- BLE scanning detects a test beacon advertising a modnet Agent Card URL
- mDNS discovers a node on the same wifi network
- NFC tap between two devices exchanges Agent Card URLs
- Share sheet sends/receives content and routes to the sovereign node
- Haptic feedback fires on server request
Not in scope: Enterprise auth, offline caching.
Goal: Enterprise authentication, persistent connection management, offline resilience, accessibility, and performance.
Scope:
- OIDC/SSO authentication for enterprise nodes
- Auto-connect on launch for saved nodes
- Offline card caching and action queuing
- Theme adaptation (native chrome matches server theme)
- Accessibility (VoiceOver/TalkBack with server-generated semantic HTML)
- Performance profiling
- Ephemeral connection lifecycle (auto-disconnect on geofence exit, wifi leave)
iOS implementation:
- OIDC/SSO:
ASWebAuthenticationSessionfor OAuth/OIDC flows - Auto-connect: iterate saved nodes with
autoConnect: trueon launch - Offline:
WKWebViewsnapshot for caching last-rendered cards,OperationQueuefor queued actions replayed on reconnect - Theme: read CSS custom properties via JavaScript bridge, apply to
UINavigationBarAppearanceand status bar style - Accessibility: ensure
WKWebViewcontent reaches VoiceOver; native chrome usesaccessibilityLabel/accessibilityTraits - Performance: instrument WebSocket throughput, render latency, WebView memory per node, node-switching time
Android implementation:
- OIDC/SSO:
AppAuthlibrary or Chrome Custom Tabs - Auto-connect: coroutine-based connection loop on saved nodes
- Offline: WebView cache mode + DataStore for queued actions
- Theme: read CSS custom properties via JavaScript bridge, apply to
MaterialThemeandWindowInsetsController - Accessibility: ensure WebView content reaches TalkBack; Compose
semanticsmodifiers on native elements - Performance: same instrumentation targets
Acceptance criteria:
- Enterprise OIDC flow completes and establishes a persistent connection
- App launches and auto-connects to all saved nodes within 3 seconds
- Disconnection shows last-rendered card; reconnection replays queued actions
- Theme changes from the server propagate to native chrome
- VoiceOver/TalkBack can navigate all server-rendered content
- No WebView memory leaks after repeated node connect/disconnect cycles
These prompts are designed to be handed to Claude Code or Codex per-slice. Each prompt references this spec for full context.
Read the attached modnet-browser-app-spec.md (full spec) and build
onbraid iOS Slice 1: Minimum Viable Browser as a SwiftUI app.
Build a thin-client iOS app that:
1. Authenticates to a Plaited server node via WebAuthn passkey
2. Loads the server URL in a WKWebView after auth
3. The WebView establishes its own WebSocket (the native layer does NOT
manage WebSocket directly — the Plaited controller protocol JS does)
4. Server-generated HTML renders; user interacts via p-trigger elements
5. QR scanner (AVCaptureSession + AVMetadataObjectTypeQRCode) extracts URLs
6. Pull-up agent conversation as a native bottom sheet overlay
7. APNs push registration; wake-up notification taps reopen and reconnect
Technical:
- SwiftUI lifecycle, minimum iOS 17
- WKWebView in UIViewRepresentable
- ASAuthorizationController for WebAuthn (AuthenticationServices)
- Monitor WebSocket state via JS bridge:
window.webkit.messageHandlers.connectionState.postMessage(state)
- On disconnect: native "Reconnecting..." overlay
- No third-party dependencies — Apple frameworks only
Do NOT build: multi-node, custom gestures, BLE/NFC/mDNS, offline cache, OIDC.
Project structure: ContentView, WebViewWrapper, AuthManager,
QRScannerView, NotificationManager as separate files.
Read the attached modnet-browser-app-spec.md (full spec) and build
onbraid Android Slice 1: Minimum Viable Browser as a Kotlin/Compose app.
Build a thin-client Android app that:
1. Authenticates to a Plaited server node via WebAuthn passkey
2. Loads the server URL in an Android WebView after auth
3. The WebView establishes its own WebSocket (native layer does NOT
manage WebSocket — the Plaited controller protocol JS does)
4. Server-generated HTML renders; user interacts via p-trigger elements
5. QR scanner (CameraX + ML Kit BarcodeScanning) extracts URLs
6. Pull-up agent conversation as BottomSheetScaffold overlay
7. FCM push registration; wake-up notification taps reopen and reconnect
Technical:
- Kotlin + Jetpack Compose, minimum SDK 28
- WebView via AndroidView in Compose
- AndroidX CredentialManager for WebAuthn
- Monitor WebSocket state via addJavascriptInterface bridge
- WebSettings: JS enabled, DOM storage enabled
- On disconnect: Compose overlay "Reconnecting..."
Do NOT build: multi-node, custom gestures, BLE/NFC/mDNS, offline cache, OIDC.
Dependencies: androidx.credentials, camerax, mlkit-barcode, firebase-messaging.
No other third-party deps.
Project structure: single-activity Compose app with WebViewScreen,
AuthManager, QRScannerScreen, NotificationService as separate files.
Desktop does not need a native app for Phase 1-2. The Plaited server already serves HTML to browsers. A PWA manifest + minimal service worker (push notification display only) is sufficient. The service worker does NOT cache content or intercept fetches — it handles push display and click-to-open only. Add the manifest and service worker to the Plaited server's static assets, not as a separate project.
If a native desktop app becomes necessary later (for BLE/NFC on desktop or tighter OS integration), evaluate Compose Desktop (reuses Kotlin/Android code) or native SwiftUI for macOS (reuses iOS code via Mac Catalyst).