# Quickstart

Open a session from a browser page and read the first press from a client, in four steps.

Working with a coding agent? Paste this line into it, and it does the steps for you:

```prompt
Set up Bitskiff: read https://docs.dev.bitskiff.com/setup.md
```

## 1. Your key

Nothing to install on Node 22 or newer: the CLI runs through `npx`. Start here:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz whoami
```

Then run `init` in a new, empty directory:

```sh
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz init
```

- If whoami's `credential` row names a key, `init` runs with no sign-in.
- If it says `(not set)`, `init` asks you to approve it once in the
  [console](https://console.dev.bitskiff.com).

`init` makes a new project named for the directory, or uses the one the directory's `bitskiff.json`
names. To use an existing project, run it with `--project <slug>`.

It writes the project's host key
to `bitskiff.json` as `key`, minting one if the project has none. The host key is the publishable
key a host page opens with. It writes the page of steps 2 and 3 as `index.html` with the key in
place.

- **Empty directory**: steps 2 and 3 are done. Serve the page with `npx serve .` and go to step 4.
- **Directory that is not empty**: `bitskiff.json` is the one file it writes, unless you pass
  `--scaffold`. Then do step 2 by hand.

The host key (`bsk_pk_...`) belongs in page source, and a new one already allows `http://localhost` and
`http://127.0.0.1` on any port. Signing in needs an invitation to an organization: see
[First sign-in](/docs/guides/console/#first-sign-in).

### If init refuses

- Refused `invalid_key`? The stored key no longer works: run
  `npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz logout`, then `init` again.
- Refused `forbidden` with `details.reason` of `missing_permission`? The key lacks the permission
  `details.permission` names: run `logout`, then `init` to sign in again.
- Refused `forbidden` without it? The key may not use that project: pass `--project` of a project
  the key may use.
- Is the key from `BITSKIFF_ORG_KEY`? `logout` does not remove it: unset the variable instead.

## 2. Your first session

Save this as `index.html`, with the `key` from `bitskiff.json` in place of `bsk_pk_...`. The import map is what makes `@bitskiff/host` resolve with no bundler, so nothing is
installed:

```html
<script type="importmap">
{
  "imports": {
    "@bitskiff/host": "https://docs.dev.bitskiff.com/sdk/host@0.3.0.js"
  },
  "integrity": {
    "https://docs.dev.bitskiff.com/sdk/host@0.3.0.js": "sha384-t4tevb6IaP3UssGRlkmHoQscYfON1BN2JyGY3zsoEGm3KmG2W3Mbe4icXcXQ7uGA"
  }
}
</script>

<script type="module">
  import { BitskiffHost, isBitskiffError, qrSvg } from '@bitskiff/host';

  try {
    const session = await new BitskiffHost({ key: 'bsk_pk_...' }).open();
    document.body.innerHTML = qrSvg(session.joinCode.url);
  } catch (err) {
    if (!isBitskiffError(err)) throw err;
    document.body.textContent = `could not open: ${err.code}`;   // `origin_not_allowed`, ...
  }
</script>
```

Serve it over HTTP, not from disk -- `npx serve .` does -- since a page opened from disk has no
origin and the open is refused.

The `integrity` block pins the module to a hash, which is what makes it safe to load code from
another origin on a page that holds your key. A page you ship keeps that entry and names the exact
version. [No build step](/docs/guides/script-tag/#version-and-integrity) has both rules.

To try the newest dev build, import `https://docs.dev.bitskiff.com/sdk/host@dev.js` with no `integrity` entry.
It changes with every dev roll, so it is not for a live app.

`open()` retries what a wait alone fixes -- a network failure, a service that is not ready, the
pacing a rate limit named -- and rejects with everything else on the first answer, a key at its
session ceiling included. That is what the `catch` draws
([`open()` rejections](/docs/guides/lifecycle/#open-rejections) lists every code).

## 3. A press and an answer

Replace the `try` block: declare the channels, listen, then open.

```html
<script type="importmap">
{
  "imports": {
    "@bitskiff/host": "https://docs.dev.bitskiff.com/sdk/host@0.3.0.js"
  },
  "integrity": {
    "https://docs.dev.bitskiff.com/sdk/host@0.3.0.js": "sha384-t4tevb6IaP3UssGRlkmHoQscYfON1BN2JyGY3zsoEGm3KmG2W3Mbe4icXcXQ7uGA"
  }
}
</script>

<script type="module">
  import { BitskiffHost, isBitskiffError, qrSvg } from '@bitskiff/host';

  try {
    const host = new BitskiffHost({ key: 'bsk_pk_...' });

    const press = host.channel('press');            // declare every channel before open
    const readout = host.channel('readout');

    press.on('message', (m) => {                    // only `press` reaches this handler
      readout.send(m.client, `pressed ${m.data}`);  // back to the client that pressed
    });

    const session = await host.open();              // open last: it seals the channels

    document.body.innerHTML = qrSvg(session.joinCode.url);  // the QR, on your page

    session.on('clientJoined', () => {              // somebody is listening now
      readout.broadcast('ready');                   // the same channel, to every client
    });
  } catch (err) {
    if (!isBitskiffError(err)) throw err;
    document.body.textContent = `could not open: ${err.code}`;   // `origin_not_allowed`, ...
  }
</script>
```

`m.client` is the client that sent the message: pass it to `send` to answer that one, or
`broadcast` to reach every client listening when it is sent. See
[Channels](/docs/guides/channels/).

## 4. The join code

Put the QR where a camera can see it, or open `session.joinCode.url` in a second tab: both land on
the [sample client page](https://join.dev.bitskiff.com/), which shows a button, a joystick and a text field. A press there
arrives in your handler, and the line you send back shows on that client.

The sample client page needs no key. A client page of your own joins with a client key, the other
kind of publishable key. `init --join-url <url>` makes one for your page's origin and saves it in
`bitskiff.json` as `clientKey`: see [Client pages](/docs/guides/client-page/#custom-client-pages).

The SDK never guesses whether a person is looking; your client page tells it. An away client keeps
its place for 30 seconds by default. The import map makes `@bitskiff/client` resolve the same way
step 2's does:

```html
<script type="importmap">
{
  "imports": {
    "@bitskiff/client": "https://docs.dev.bitskiff.com/sdk/client@0.3.0.js"
  },
  "integrity": {
    "https://docs.dev.bitskiff.com/sdk/client@0.3.0.js": "sha384-d2cF/4NoPsVP5hrsmWr6yvH7578xCVoloemqaTxgXX6L5wd03MbZo1jrX1G0MJLa"
  }
}
</script>

<script type="module">
  import { BitskiffClient } from '@bitskiff/client';

  const client = new BitskiffClient({ key: 'bsk_pk_...' });   // your client key

  document.addEventListener('visibilitychange', () =>
    client.setPresence(document.visibilityState === 'hidden' ? 'away' : 'present'));
</script>
```

The sample client page allows 100 joins a day per project, which clears at 00:00 UTC. See
[Limits and capacity](/docs/guides/limits/).

There is a [sample host](/host/) too, for a session and a join code without writing a page.

## Next steps

### Build

- [Channels](/docs/guides/channels/)
- [Client pages](/docs/guides/client-page/)
- [Controller input](/docs/guides/controller/)
- [Node hosts](/docs/guides/node-hosts/)
- [No build step](/docs/guides/script-tag/)
- [Downloads](/docs/downloads/), every package as a tarball

### Operate

- [Session lifecycle](/docs/guides/lifecycle/)
- [Limits and capacity](/docs/guides/limits/)
- [Webhooks](/docs/guides/webhooks/)

### Reference

- [@bitskiff/host](/docs/packages/host/), the host reference
- [@bitskiff/client](/docs/packages/client/), the client reference
