Your own phone page
A new project's join code opens our sample phone page, so a first host needs no phone code at all. The day you ship, the phone page is yours.
The sample page
It is one page for everybody—a button, a joystick and a text field, with a readout your host writes—and it draws those same three controls whatever host it joins. It is live at /join/, which is where a scanned code lands, and its address is the join URL a new project starts with; the console's project settings is where you see it and where you replace it.
One control per channel, and each message is the value itself. Nothing carries a tag, because the channel is what says which kind of message it is:
| Channel | Delivery | Who sends | Each message is |
|---|---|---|---|
press | reliable | the page | "down" when the button goes down, "up" when it comes back |
text | reliable | the page | the word that was typed, "hello" |
move | lossy | the page | { "x": 0.5, "y": -0.2 }, sixty times a second |
readout | reliable | the host | a string the page shows in its readout |
The joystick is the one row that is JSON, because two numbers need two names. A press and a word
arrive as the value and nothing around it, so a host reads them with no JSON.parse at all:
import { BitskiffHost, everyone } from '@bitskiff/host';
const session = await new BitskiffHost({ key: 'mrc_pk_...' }).open();
session.channel('press').on('message', (m) => console.log('press', m.from.id, m.data));
session.channel('text').on('message', (m) => console.log('typed', m.from.id, m.data));
session.channel('move', { delivery: 'lossy' }).on('message', (m) => {
if (typeof m.data !== 'string') return; // this page never sends bytes
const { x, y } = JSON.parse(m.data) as { x: number; y: number };
console.log('move', x, y);
});
session.channel('readout').send(everyone, 'you are on');
Those four names are this page's own. Nothing on the platform checks them, no session is refused for sending something else, and your own phone page never has to know they exist. The page is for getting started: a project still on it at 100 sessions is warned in the console and on the command line, and at 1000 its phones are refused with a row saying the project has a join page to set.
A page of your own
npm install @bitskiff/client
import { BitskiffClient } from '@bitskiff/client';
const client = new BitskiffClient();
await client.start('1CSESSION1ABC123'); // resolves when you are seated
const press = client.channel('press');
client.channel('readout').on('message', (m) => console.log(m.data));
press.send('down');
It is the host side's mirror image—the same names, the same deliveries, the same string or bytes—with no to on a send, because a phone has one host.
You pass the code. The SDK never reads location and never parses a link. The host's
session.joinCode.code is exactly the string start() takes: your page finds it in whatever its
own link carries and hands it over unchanged. Do not split it or trim it—it carries more than
the digits a person reads, and a page that passes half of it joins with nothing to check the host
against.
Point the sessions at your page
Set joinUrl to it in the console's project settings. That is
the whole of the setup: the page
needs no key of its own and no allowlist entry.
Setting it also turns the guard on. A session's join URL says which client the session is for, so
from that moment a phone at your page joins your sessions and a copy of @bitskiff/client running
somewhere else does not: it is refused before it takes a seat, with wrongClient. Scheme, host and
port are compared—https://play.example.com and https://play.example.com:8443 are two origins—and paths, queries and fragments stay yours.
Serve it with a CSP header
Serve the page with a Content-Security-Policy response header carrying at least
frame-ancestors 'none'. The <meta http-equiv> form does not count—browsers ignore
frame-ancestors there—and a framed page holding a live seat is somebody else's session taking
the taps.
A page has to be served either way: opened from disk it has no origin, and the join is refused.
What the page shows
Five arms cover every state a phone is in once it has started:
import { BitskiffClient } from '@bitskiff/client';
const client = new BitskiffClient();
const show = (banner: string) => console.log(banner);
client.on('state', (s) => {
switch (s.phase) {
case 'joining':
return show('connecting');
case 'connected':
return show(s.host === 'away' ? 'the host stepped out' : 'ready');
case 'reconnecting':
return show('reconnecting');
case 'suspended':
return show('try again'); // `client.retry()` is what the button calls
case 'ended':
return show(s.ended?.reason ?? 'ended');
}
});
await client.start('1CSESSION1ABC123');
suspended is the SDK having stopped trying on its own with the seat not known to be gone;
client.retry() starts it again, and start() stays a once-per-handle call. Only ended is final.
Native apps
A native app, a signage player or a game engine sends no browser origin, so it says where it is once:
import { BitskiffClient } from '@bitskiff/client';
const client = new BitskiffClient({ origin: 'https://play.example.com' });
await client.start('1CSESSION1ABC123');
That is the client's own word for it: it keeps a code from being scanned into the wrong app.
A load generator, a test harness or an embedded controller uses createNodeClient from
@bitskiff/client/node—the same factory with the browser's pieces swapped for the runtime's, so
nothing else about the session changes and there is no second package to install.