> ## Documentation Index
> Fetch the complete documentation index at: https://crosslink.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Mobile Bootstrap

> What happens between the QR scan and your mobile app

When someone scans a Crosslink QR, Crosslink runs the whole onboarding. Your
mobile page is handed a trusted, connected RPC channel and nothing else.

```text theme={null}
Desktop app
    ↓  createPairingCard
QR scanned
    ↓
Crosslink mobile bootstrap        ← pairing, code entry, SAS confirmation
    ↓
Trust persisted on the device
    ↓
Add to Home Screen  ── or ──  Continue in browser
    ↓
crosslink.onConnected(rpc)        ← your app starts here
```

Every screen above the last line is Crosslink's. You do not build, style or
replace them.

## Turning it on

Declare your application and point at your mobile page:

```ts theme={null}
import { createCrosslinkServer } from "@crosslink/sdk-node";

const host = createCrosslinkServer({
  application: {
    id: "com.example.notes",
    name: "Example Notes",
    shortName: "Notes",
    icon: "/icon-192.png",
    accentColor: "#f97316",
    backgroundColor: "#101014",
    appearance: "dark"
  },
  mobile: { entry: "./mobile/index.html" },
  capabilities: [{ id: "notes.read", title: "Read notes", risk: "low" }]
});

host.expose("notes.list", () => notes, { capability: "notes.read" });
await host.start();
```

That is the entire mobile setup. From `mobile.entry` alone, Crosslink serves:

| Path                       | What it is                                            |
| -------------------------- | ----------------------------------------------------- |
| `/`                        | Your page, with the installable head injected         |
| `/manifest.webmanifest`    | Generated from your `application` metadata            |
| `/sw.js`                   | Generated service worker, at root scope               |
| `/__crosslink/sdk.js`      | The browser SDK — you need no bundler                 |
| `/__crosslink/boot.js`     | Boot script carrying your metadata                    |
| `/__crosslink/icon-*.png`  | Your icon, or a generated one                         |
| `/__crosslink/install/:id` | Install handoff, so an installed app does not re-pair |

In host-served mode these assets ride on the Crosslink transport port. On plain
LAN HTTP that is convenient direct browser access, not an installable offline
PWA. In secure published-bootstrap mode the stable HTTPS application origin is
separate from the desktop transport and connects only through a permitted
`wss://` route. See [Durable Origins](/client/durable-origin).

## What your mobile page looks like

Your markup, plus one callback:

```html theme={null}
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>Notes</title>
</head>
<body>
  <ul id="notes" hidden></ul>

  <script>
    crosslink.onConnected(async (rpc) => {
      document.getElementById("notes").hidden = false;
      render(await rpc.call("notes.list"));
    });

    crosslink.onDisconnected(() => {
      document.getElementById("notes").hidden = true;
    });
  </script>
</body>
</html>
```

No manifest link. No apple-touch-icon. No `navigator.serviceWorker.register`.
No SDK script tag. No pairing screen. Crosslink injects what the page needs and
publishes `window.crosslink` before any of your scripts run.

`onConnected` fires again after every reconnect, so there is no reconnect
handling to write either.

### The `crosslink` object

| Member               | Purpose                                                                   |
| -------------------- | ------------------------------------------------------------------------- |
| `onConnected(fn)`    | Runs now if connected, and after every reconnect. Returns an unsubscribe. |
| `onDisconnected(fn)` | Runs when the trusted channel goes away.                                  |
| `onStateChange(fn)`  | Bootstrap state transitions.                                              |
| `rpc`                | The current channel, or `null`.                                           |
| `reset()`            | Clears this device's identity and pairs again.                            |

Corresponding DOM events (`crosslink:connected`, `crosslink:disconnected`,
`crosslink:state`) fire too, for pages that prefer listeners.

## The Crosslink attribution footer

The authorized mobile app shell carries the Crosslink attribution footer, and
the bootstrap mounts it for you once your page takes over. You write no markup
for it. It participates in normal layout flow, spans the page width, centers the
text, and links to the Crosslink repository:

```text theme={null}
End-to-end encrypted with crosslink
```

Its presentation can be tuned:

```ts theme={null}
createCrosslinkServer({
  application: { id: "com.example.notes", name: "Example Notes" },
  mobile: {
    entry: "./mobile.html",
    attribution: {
      color: "#94a3b8",
      background: "transparent",
      size: 11,
      offset: 12
    }
  }
});
```

| Field        | What it does                              |
| ------------ | ----------------------------------------- |
| `color`      | Text colour of the footer.                |
| `background` | Footer background.                        |
| `size`       | Font size; a number is pixels.            |
| `offset`     | Footer block padding; a number is pixels. |
| `className`  | Extra class, for your own styling.        |

There is no field that removes the footer or changes its wording. Colour, size,
background and spacing are the whole surface. `writeStaticBootstrap()` takes the
same `attribution` object, so a published static origin looks the same as the
one your host serves.

<Note>
  The desktop pairing card is the other side of this: it draws the Crosslink
  wordmark in its own column and carries no attribution footer. See
  [Pairing Card](/client/pairing-card).
</Note>

## Mounting it on a server you already run

A host with its own HTTP server can mount the same handler instead of letting
Crosslink attach it to the transport port:

```ts theme={null}
const bootstrap = host.createBootstrapHandler();
myServer.use((req, res) => bootstrap(req, res));
```

## Using the bootstrap directly

`CrosslinkMobileBootstrap` is the class behind all of this, and it is public.
Construct it yourself only when you are not serving the page from a Crosslink
host — a native shell, or a page hosted somewhere Crosslink cannot inject into:

```ts theme={null}
import { CrosslinkMobileBootstrap } from "@crosslink/sdk-browser";

new CrosslinkMobileBootstrap({
  appId: "com.example.notes",
  appName: "Example Notes",
  capabilities: ["notes.read"],
  onAuthorized: (rpc) => mountNotes(rpc),
  onUnauthorized: () => unmountNotes()
}).start();
```

The screens are identical either way. `mobile.entry` is the same thing with the
metadata filled in from your host.

## Before you ship

Add to Home Screen and the cached offline screen depend on the origin the page
is served from, not on Crosslink. Read
[Durable Origins](/client/durable-origin) before you assume a LAN address is
enough — it is not, and the failure only shows up on a phone.
