Bitskiff

Quickstart

On this page

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:

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

Then run init in a new, empty directory:

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

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.

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

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

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.

There is a sample host too, for a session and a join code without writing a page.

Next steps

Build

Operate

Reference