JavaScript

@blackevin/client — one package for Node, the browser, Bun and Deno, with the realtime client and the REST client.

Install

npm install @blackevin/client
pnpm add @blackevin/client

Its only dependency is @blackevin/protocol — the wire format, with no dependencies of its own and safe in a browser.

One package, two runtimes

Node and the browser are not two SDKs. @blackevin/client is one package that runs in both — same classes, same methods, same wire protocol. There is nothing to choose between and nothing extra to install for one or the other.

What differs is not the API. It is what the code is allowed to hold:

server — Node, Bun, Denobrowser
credentialkey, the API key itselfauthUrl or token. Never the key
endpointBLACKEVIN_ENDPOINT read from the environmentpassed in, because there is no environment at runtime
usual entry pointBlackevin.Rest — publish without holding a socketBlackevin.Realtime — the socket is the point

Both entry points exist in both runtimes; the table is what each is normally for, not a restriction.

What you get

exportwhat it is
Blackevin.Realtimethe WebSocket client — channels, presence, connection state
Blackevin.Restthe HTTP client — publish, history, presence, token requests
Blackevin.BlackevinErrorthe error type, carrying statusCode and a stable reason slug

Blackevin.Blackevin is an alias of Realtime. Everything exported is public API.

From a server

import * as Blackevin from '@blackevin/client';

const rest = new Blackevin.Rest({ key: process.env.BLACKEVIN_KEY });

await rest.channels.get('orders:new').publish('order.created', { id: 7 });

No socket, no connection to keep open. From a server has the token endpoint and the rest of the REST client.

From a browser

import * as Blackevin from '@blackevin/client';

const realtime = new Blackevin.Realtime({
  authUrl: '/api/blackevin-token',
  clientId: String(user.id),
});

const channel = realtime.channels.get('chat:lobby');

channel.subscribe(message => console.log(message.data));

The API key does not appear here, and must not.

The browser has no environment to read, so a web app that needs a non-default endpoint passes its own build's value:

new Blackevin.Realtime({
  endpoint: import.meta.env.VITE_BLACKEVIN_WS,
  authUrl: '/api/blackevin-token',
});

Unset resolves to undefined, which falls through to the default — so the same line works in production without a branch.

Where next

pagewhat is in it
Connectevery client option, and what the handshake does
Channelssubscribe, publish, rewind, offline queueing
Presenceenter, leave, update, and reading the member list
Historypast messages and past presence events
Connection statethe state machine and the reconnect policy
From a serverBlackevin.Rest: the token endpoint, publish, history
React, Next.js, Solidwriting your own hook, since there is no framework package

Errors

Both clients reject with BlackevinError:

try {
  await channel.publish('status', payload);
} catch (error) {
  if (error instanceof Blackevin.BlackevinError) {
    error.statusCode; // 403, 429, …
    error.reason;     // 'connection_limit', 'queue_limit', 'message_rate_limit', or undefined
    error.isQuota;    // a plan ceiling — show an upgrade prompt, retry later
    error.message;    // the server's own sentence; do not branch on it
  }
}

Branch on statusCode and reason. The message is written for a person.

ESM and CommonJS

Both. The package publishes an ESM build and a CommonJS one, and the exports map points each at the right files, types included:

import * as Blackevin from '@blackevin/client';
const Blackevin = require('@blackevin/client');

Nothing to configure — your loader picks. TypeScript gets .d.ts on the import side and .d.cts on the require side, so the types resolve either way.

The build targets ES2022, and Node 22 is the floor.

On this page