JavaScript
@blackevin/client — one package for Node, the browser, Bun and Deno, with the realtime client and the REST client.
Install
npm install @blackevin/clientpnpm add @blackevin/clientIts 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, Deno | browser | |
|---|---|---|
| credential | key, the API key itself | authUrl or token. Never the key |
| endpoint | BLACKEVIN_ENDPOINT read from the environment | passed in, because there is no environment at runtime |
| usual entry point | Blackevin.Rest — publish without holding a socket | Blackevin.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
| export | what it is |
|---|---|
Blackevin.Realtime | the WebSocket client — channels, presence, connection state |
Blackevin.Rest | the HTTP client — publish, history, presence, token requests |
Blackevin.BlackevinError | the 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
| page | what is in it |
|---|---|
| Connect | every client option, and what the handshake does |
| Channels | subscribe, publish, rewind, offline queueing |
| Presence | enter, leave, update, and reading the member list |
| History | past messages and past presence events |
| Connection state | the state machine and the reconnect policy |
| From a server | Blackevin.Rest: the token endpoint, publish, history |
| React, Next.js, Solid | writing 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.