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:
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:
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz whoami
Then run init in a new, empty directory:
npx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz init
- If whoami's
credentialrow names a key,initruns with no sign-in. - If it says
(not set),initasks 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.jsonis 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: runnpx --yes https://docs.dev.bitskiff.com/downloads/0.3.0/bitskiff-cli.tgz logout, theninitagain. - Refused
forbiddenwithdetails.reasonofmissing_permission? The key lacks the permissiondetails.permissionnames: runlogout, theninitto sign in again. - Refused
forbiddenwithout it? The key may not use that project: pass--projectof a project the key may use. - Is the key from
BITSKIFF_ORG_KEY?logoutdoes 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:
<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.
<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:
<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
- Channels
- Client pages
- Controller input
- Node hosts
- No build step
- Downloads, every package as a tarball
Operate
Reference
- @bitskiff/host, the host reference
- @bitskiff/client, the client reference