meeet-sdk
v0.1.0
Published
Typesafe TypeScript SDK for the Meeet API (REST client + signaling WebSocket)
Maintainers
Readme
meeet-sdk
Typesafe TypeScript SDK for the Meeet app — a typed REST client for the FastAPI backend plus a typed WebSocket client for the Cloudflare signaling relay.
- Zero runtime dependencies — works in the browser, Node 18+, Bun, and React Native.
- Framework-agnostic — nothing to configure, pass it to any UI layer.
- Automatic token refresh — on a
401the client transparently refreshes and retries. - Typed everywhere — request/response models and signaling messages are discriminated unions.
- Works without the DOM lib in the type system —
d.tsis self-contained, so RN/Node TS builds compile.
Install
npm install meeet-sdkNot published yet? Use a local install:
npm install ../meeet/sdk # from a sibling project
# or
npm link ../meeet/sdkRequires fetch (browser, Node ≥ 18, RN) and, for signaling, a global WebSocket
(browser, Node ≥ 22, RN, Bun).
Quick start
import { MeeetClient } from 'meeet-sdk';
const client = new MeeetClient(); // defaults to https://meeet-api.vercel.app
await client.auth.register({
name: 'Ada',
email: '[email protected]',
password: 'hunter2-secret',
});
// tokens are stored automatically
const me = await client.auth.me();
const users = await client.users.list({ limit: 100 });REST client — MeeetClient
new MeeetClient({
baseUrl?: string; // default 'https://meeet-api.vercel.app'
tokenStore?: TokenStore; // default in-memory
accessToken?: string; // initial tokens (only when no tokenStore given)
refreshToken?: string;
fetchImpl?: FetchLike; // inject custom fetch (timeouts, mocks, ...)
onUnauthorized?: () => void | Promise<void>; // refresh failed -> redirect to login
})Endpoints
client.auth
| Method | Signature | Returns | Notes |
| --- | --- | --- | --- |
| register | ({ name, email, password }) | AuthResponse | Stores tokens. 400 if email taken. |
| login | ({ email, password }) | TokenResponse | Stores tokens. 401 on bad credentials. |
| refresh | () | Promise<boolean> | Exchanges stored refresh token; false + clears store when unusable. |
| logout | () | Promise<void> | Revokes refresh token, clears store. |
| me | () | User | Current user. |
client.users
| Method | Signature | Returns | Notes |
| --- | --- | --- | --- |
| list | ({ skip, limit }) | User[] | Paginated directory. |
| get | (id) | User | — |
| update | (id, { name?, email?, is_active? }) | User | Self only. |
| delete | (id) | void | Self only. |
client.avatar
| Method | Signature | Returns | Notes |
| --- | --- | --- | --- |
| upload | ({ data, content_type? }) | User | data is base64 or data:...;base64,...; png/jpeg/gif/webp; ≤ 2MB raw. |
| delete | () | User | Clears avatar. |
Token lifecycle
register/loginwrite the returned pair into theTokenStore.- Every request sends
Authorization: Bearer <access_token>. - On
401, the client calls/auth/refreshonce, stores the new pair, and retries the original request (concurrent requests share a single refresh). - If refresh fails, tokens are cleared and
onUnauthorizedruns — a good place to navigate to a login screen.
Errors
ApiError: non-2xx response. Has.statusand.detail(FastAPI'sdetail);messageis human-readable.NetworkError: request could not be sent (offline, DNS, timeout).SignalingError: WebSocket-level failures.
import { ApiError } from 'meeet-sdk';
try {
await client.auth.login({ email, password });
} catch (err) {
if (err instanceof ApiError && err.status === 401) {
// wrong credentials
}
}Token stores
Plug in any persistence with the tiny TokenStore interface
(getTokens / setTokens / clear, all sync).
| Store | Platform | Notes |
| --- | --- | --- |
| MemoryTokenStore (default) | everywhere | In-memory; lost on restart. |
| LocalStorageTokenStore | browser | No-ops safely on non-browser platforms (won't persist there). |
| createAsyncKvTokenStore(kv) | RN, Node, serverless | Sync mirror + async persistence to any KV store. |
React Native
Use createAsyncKvTokenStore with your storage of choice, and hydrate once at startup:
AsyncStorage (method names match 1:1):
import AsyncStorage from '@react-native-async-storage/async-storage';
import { MeeetClient, createAsyncKvTokenStore } from 'meeet-sdk';
const tokens = createAsyncKvTokenStore(AsyncStorage);
await tokens.hydrate(); // load saved session before use
const client = new MeeetClient({ tokenStore: tokens });expo-sqlite / kv-store (async-suffixed methods — small adapter):
import { Storage } from 'expo-sqlite/kv-store';
import { MeeetClient, createAsyncKvTokenStore } from 'meeet-sdk';
const kv = new Storage('meeet.db');
const tokens = createAsyncKvTokenStore({
getItem: (key) => kv.getItemAsync(key),
setItem: (key, value) => kv.setItemAsync(key, value),
removeItem: (key) => kv.removeItemAsync(key),
});
await tokens.hydrate();
const client = new MeeetClient({ tokenStore: tokens });react-native-mmkv / expo-secure-store — same pattern: map getItem / setItem / removeItem. No SDK changes needed.
Signaling — SignalingClient
Typed wrapper around the Cloudflare Workers relay. Messages relayed live between
peers in a room (dm-{min}-{max} style room ids work well for 1:1 chat/calls).
import { SignalingClient } from 'meeet-sdk';
const sig = new SignalingClient({ peer: String(me.id) });
await sig.connect(room);
sig.on('joined', (m) => console.log('assigned peer', m.peer));
sig.on('peers', (m) => console.log('people in room:', m.count));
sig.on('chat', (m) => showChat(m.from, m.text, m.ts));
sig.send({ type: 'chat', from: me.id, fromName: me.name, text: 'hello' });Options
new SignalingClient({
baseUrl?: string; // default wss://meeet-signaling.meeet-signaling-server.workers.dev
peer?: string; // your peer id (?peer=); generated if omitted
key?: string; // relay shared secret (?key=) if the server requires it
autoReconnect?: boolean; // default true
maxRetries?: number; // default 3
reconnectDelayMs?: number; // default 1000 (doubles per attempt)
WebSocketImpl?: ctor; // inject a custom WebSocket class
})Subscribing
on('type', handler) narrows the payload via the SignalingMessage union —
handlers are fully typed; onRaw(handler) gets every message. Both return an
unsubscribe function. join message resolution is await sig.connect(room).
Message types
| type | Direction | Fields |
| --- | --- | --- |
| joined | server → you | peer (id assigned by server) |
| peers | server → all | count (connections in room) |
| chat | client → relay | from, fromName?, text, ts |
| call-offer | client → relay | sdp |
| call-answer | client → relay | sdp |
| call-ice | client → relay | candidate |
| call-hangup | client → relay | — |
WebRTC call walkthrough
Messaging is live relay only (no storage). Calls are peer-to-peer WebRTC with public STUN (no TURN — strict NATs may fail).
sig.on('call-offer', async (offer) => {
const pc = peerConnection();
await pc.setRemoteDescription({ type: 'offer', sdp: offer.sdp });
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sig.send({ type: 'call-answer', sdp: answer.sdp });
});
sig.on('call-answer', async (answer) => {
await pc.setRemoteDescription({ type: 'answer', sdp: answer.sdp });
});
sig.on('call-ice', async ({ candidate }) => {
await pc.addIceCandidate(candidate);
});
pc.onicecandidate = (e) => {
if (e.candidate) sig.send({ type: 'call-ice', candidate: e.candidate.toJSON() });
};Calling side creates the offer; hang up with sig.send({ type: 'call-hangup' }).
React Native note
- At runtime
fetchandWebSocketare provided by RN, so both clients work out of the box. - The SDK's
.d.tsuses no DOM-lib globals, so it type-checks in RN projects whosetsconfigomits the DOM lib. - Use
createAsyncKvTokenStore(above) for persistent, non-blocking token storage. - Camera/microphone require HTTPS and
getUserMedia, as usual on RN.
Development
npm run build # tsc -> dist/ (ESM + .d.ts with JSDoc)
node test/smoke.mjs # 20 checks against the LIVE backend + relayTypecheck the public types from a consumer's perspective:
npx tsc --noEmit --strict --lib ES2020 test/types.check.ts # no DOM lib: RN-safeAPI surface
Exports: MeeetClient, SignalingClient, ApiError, NetworkError,
SignalingError, MemoryTokenStore, LocalStorageTokenStore,
createAsyncKvTokenStore, constants DEFAULT_API_BASE_URL,
DEFAULT_SIGNALING_BASE_URL, and all model types (User, AuthResponse,
SignalingMessage, …). Every export is documented in its JSDoc and the shipped .d.ts.
