Skip to content

Host Handoff Flow

The loopback/cookie/handoff sequence is what a consumer host shell implements. Both reference shells (examples/android-app and examples/ios-app) do it; here is the exact flow.

  1. The host starts the worklet with three sandbox dirs: [<webAssets>, <storage>, <cache>]. webAssets is your built web app; storage is durable (photos, p2p corestore); cache is ephemeral.

  2. The worklet binds 127.0.0.1 on an ephemeral port and writes handoff.json ({ origin, port, bootstrap }) into storage. The session token is never written.

  3. The host polls for handoff.json, deletes it as soon as it has read it (BareHandoff.consume on both hosts), and injects bootstrap (the one-time nonce) into the page before any page script runs, only on the loopback origin and only in the main frame:

    window.__ekrooh = { bootstrap: '<bootstrap>' };
    window.BareShell = true;

    The page never sees the session token: page JS (third-party scripts, a consumer plugin dispatch handler) could otherwise read it and mint authorized requests to every other gated resource.

  4. The host loads http://127.0.0.1:<port>/index.html. The worklet writes the file atomically with owner-only permissions; an unredeemed nonce expires after 120 s of running time from the page load (paused while the worklet is suspended; 600 s if the page never loads).

  5. The page’s WebSocket transport reads window.__ekrooh.bootstrap, calls POST /login once (the nonce is single-use), gets the bare_session_<port> cookie, and connects to ws://127.0.0.1:<port> — same origin, so cookie-authenticated. The transport never places the credential in a URL.

  6. Media URLs returned by plugins are same-origin too — <img>/fetch ride the cookie, no token in the URL.

examples/android-app/src/main/java/to/holepunch/bare/android/ shows the full wiring. The worklet is owned by the Application (BareApplication, ReferenceHost), so it survives rotation and a second launch of the singleTop MainActivity; only the first WebView gets the nonce, later ones log in with the cookie, and a handoff that arrives after the error page is shown is still consumed and loaded. Key bits: start the worklet with [webAssets, noBackupFilesDir/bare, cacheDir/bare-cache], read the handoff with BareHandoff.consume(file), set BareWebViewClient.allowedOrigin, check WebViewFeature.DOCUMENT_START_SCRIPT and inject window.__ekrooh = { bootstrap } with addDocumentStartJavaScript(script, setOf(handoff.origin)), then loadUrl("http://127.0.0.1:<port>/index.html").

examples/ios-app/app/BareRuntime.swift does the same with the library: BareHandoff.consume(at:) reads and deletes the file, and BareWebHost.load(_:in:delegate:) injects an origin-guarded, main-frame-only WKUserScript at .atDocumentStart, loads the page, and removes the script once the first page has loaded. If the handoff is slow, the host keeps polling after showing a timeout message.

  • handoff.json must be written to the host-provided sandbox dir, never a shared location.
  • The page receives only a one-time bootstrap nonce. The session token stays inside the worklet (runtime.start() returns it) and is never accepted as a /login body. The cookie is HttpOnly; SameSite=Strict.
  • Non-browser clients use the X-Bare-Token header. There is no ?token= URL fallback.
  • All loopback responses send Referrer-Policy: no-referrer.
  • Both hosts confine the WebView to the handoff origin before the first load.