> ## 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.

# Your First App

> A complete host and phone app on one port: expose an action, emit an event, and let Crosslink handle pairing and installation.

The [Quickstart](/quickstart) gets a connection working. This page builds the
thing you would actually ship, and shows how little of it is Crosslink-specific:
a host, a list of capabilities, and a phone page that starts at
`crosslink.onConnected(rpc)`.

## 1. Project

```bash theme={null}
mkdir lamp && cd lamp && npm init -y
npm install @crosslink/sdk-node
```

One dependency. The browser SDK is served to your pages by the host, so there is
no bundler and nothing to build.

## 2. Capabilities first

Decide what a paired phone may do before writing a handler. Every capability is
a promise to the user in the pairing prompt, so name them for what they let
someone do, not for the function they call.

```js theme={null}
const capabilities = [
  { id: "lamp.read",  title: "See whether the lamp is on", risk: "low"  },
  { id: "lamp.write", title: "Turn the lamp on and off",   risk: "medium" }
];
```

`risk` drives the default policy: `low` may be auto-granted, `medium` and above
require a human decision. See [Capabilities and RPC](/build/capabilities-and-rpc).

## 3. The host

`host.mjs`:

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

let on = false;

const host = createCrosslinkServer({
  application: {
    id: "com.example.lamp",
    name: "Lamp",
    version: "1.0.0",
    shortName: "Lamp",
    accentColor: "#38bdf8",
    backgroundColor: "#0f172a",
    appearance: "dark"
  },
  // Your phone UI. Crosslink serves it on the transport port, together with the
  // manifest, service worker, icons, browser SDK and its own onboarding.
  mobile: { entry: "./mobile/index.html" },
  capabilities: [
    { id: "lamp.read",  title: "See whether the lamp is on", risk: "low" },
    { id: "lamp.write", title: "Turn the lamp on and off",   risk: "medium" }
  ],
  lan: { enabled: true, bind: "all" }
});

host
  .expose("lamp.status", () => ({ on }), { capability: "lamp.read" })
  .expose(
    "lamp.set",
    (input) => {
      on = Boolean(input?.on);
      host.emit("lamp.changed", { on });
      return { on };
    },
    { capability: "lamp.write" }
  )
  .declareEvent("lamp.changed");

await host.start();
console.log(host.describeMobileDelivery().message);
```

<Note>
  `lan.bind: "all"` matters: the phone dials the address in the QR, and loopback
  on the phone means the phone itself.
</Note>

## 4. The phone page

`mobile/index.html` is your UI and one callback. No pairing screen, no SAS
prompt, Service Worker, or manifest belongs in this file — Crosslink owns those
pieces wherever the browser/deployment supports them. By the time
`onConnected` fires, pairing and authorization are complete; it fires again
after every reconnect.

```html theme={null}
<!doctype html>
<html lang="en">
<head><meta charset="utf-8" /><title>Lamp</title></head>
<body>
  <p id="lamp">…</p>
  <button id="toggle" hidden>Toggle</button>

  <script>
    crosslink.onConnected(async (rpc) => {
      const render = ({ on }) => {
        document.getElementById("lamp").textContent = on ? "ON" : "OFF";
      };

      document.getElementById("toggle").hidden = false;
      document.getElementById("toggle").onclick = async () => {
        const { on } = await rpc.call("lamp.status");
        await rpc.call("lamp.set", { on: !on });
      };

      rpc.subscribe("lamp.changed", render);
      render(await rpc.call("lamp.status"));
    });

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

## 5. Run it

```bash theme={null}
node host.mjs
```

Show the pairing QR on a desktop page with
[`createPairingCard`](/client/pairing-card), scan it with a phone on the same
Wi-Fi, and Crosslink runs pairing and the handoff into your page. This default
plain-HTTP LAN path is direct development access, not the installed-PWA mode.

## Serving the client from the host

The single-port arrangement above is deliberate, and worth keeping even as the
app grows:

* **One mapping.** Remote access forwards exactly one port. A page on port 3000
  and a socket on port 54676 needs two, and the second one is the one people
  forget.
* **One development origin.** The host-served page and socket need no
  cross-origin exceptions. That convenient LAN origin is still today's desktop
  address, not a durable installed-app identity.
* **No route mismatch.** A page that loads cannot then fail to reach the socket,
  because they are the same listener.

If your UI is served by a dev server during development (Vite, for example),
add its origin to `lan.allowedOrigins` rather than moving the socket.

## Making it installable

Your mobile application code does not change: Crosslink already generates the
manifest, Service Worker, and icons from `application` metadata. The deployment
does change: use a stable HTTPS bootstrap origin and a permitted WSS route. The
QR points at an `https`/`http` page rather than a `crosslink://` URI because an
iPhone camera has no handler for a custom scheme.

What decides whether an install actually behaves like an app is the **origin**
the phone loaded. On a plain-HTTP LAN address the browser will not register a
service worker, so Add to Home Screen produces a bookmark with no cached offline
screen. Your host says which case you are in:

```js theme={null}
console.log(host.describeMobileDelivery().message);
```

To make installs durable, publish Crosslink's static bootstrap once and point
`pairing.bootstrapUrl` at it:

```js theme={null}
await host.writeStaticBootstrap("./dist-bootstrap");
```

[Durable Origins](/client/durable-origin) covers the trade-offs, including the
one that matters most: a published `https` origin cannot use `ws://`, so it
needs a relay or a tunnel to reach your machine.

## Next

<CardGroup>
  <Card title="Capabilities and RPC" icon="key" href="/build/capabilities-and-rpc">
    Designing the permission surface and the method surface
  </Card>

  <Card title="Connection Modes" icon="wifi" href="/guides/connection-modes">
    Same Wi-Fi, relayed, or reachable from anywhere
  </Card>

  <Card title="Production Checklist" icon="list-check" href="/build/production-checklist">
    What to fix before other people run this
  </Card>
</CardGroup>
