connectbase-client
v3.12.0
Published
Connect Base JavaScript/TypeScript SDK for browser and Node.js
Maintainers
Readme
connectbase-client
Connect Base JavaScript/TypeScript SDK for building real-time multiplayer games and applications.
Installation
npm install connectbase-client
# or
pnpm add connectbase-client
# or
yarn add connectbase-clientKey Types
Connect Base provides two types of Keys. Use the right key for your use case:
| Type | Prefix | Use For | Permissions | Safe to Expose? |
|------|--------|---------|-------------|-----------------|
| Public Key | cb_pk_ | SDK / Web apps | Limited (RLS enforced) | ✅ Yes — safe in frontend code |
| Secret Key | cb_sk_ | MCP / Admin tools | Full access (bypasses RLS) | ❌ Never expose in frontend or public repos |
Which key should I use?
| Context | Key Type | Example |
|---------|----------|---------|
| Frontend SDK (new ConnectBase()) | Public Key (cb_pk_) | Web/app: DB queries, auth, file uploads |
| .env file (VITE_CONNECTBASE_PUBLIC_KEY) | Public Key (cb_pk_) | React, Vue, etc. |
| CLI deploy (.connectbaserc) | Public Key (cb_pk_) | npx connectbase deploy |
| MCP server (AI tools) | Secret Key (cb_sk_) | Claude, Cursor, Windsurf |
| Server-side admin tasks | Secret Key (cb_sk_) | Backend full data access |
⚠️ MCP server rejects Public Keys — you must use a Secret Key (
cb_sk_).⚠️ Never use Secret Keys in frontend code — RLS is bypassed, exposing all data.
Create Keys in the Console under Settings > API tab. Choose Public or Secret type when creating. The full key is shown only once at creation time.
Quick Start
import ConnectBase from 'connectbase-client'
// Initialize the SDK — use a Public Key (cb_pk_)
const cb = new ConnectBase({
publicKey: 'cb_pk_your-public-key'
})
// Create a game room client
const gameClient = cb.game.createClient({
clientId: 'player-123'
})
// Set up event handlers
gameClient
.on('onConnect', () => console.log('Connected!'))
.on('onStateUpdate', (state) => console.log('State:', state))
.on('onDelta', (delta) => console.log('Delta:', delta.changes))
.on('onAction', (action) => console.log('Action:', action.type, action.clientId))
.on('onPlayerJoined', (player) => console.log('Player joined:', player.clientId))
.on('onPlayerLeft', (player) => console.log('Player left:', player.clientId))
// Connect and create a room
await gameClient.connect()
const state = await gameClient.createRoom({
maxPlayers: 4,
tickRate: 64
})Features
- Real-time Game Server: WebSocket-based multiplayer game state synchronization
- Authentication: ID/Password, guest login, and OAuth social login support
- Database: JSON-based NoSQL database with real-time queries
- Storage: File storage with CDN support
- Push Notifications: Cross-platform push notification support
- WebRTC: Real-time audio/video communication
- Payments: Subscription and one-time payment support
- AI Streaming: Real-time AI text generation via WebSocket (Gemini)
- Endpoint: Call your own GPU models on your own PC through one
cb_pk_*key — ConnectBase forwards the payload as-is (dumb pipe) - Support: End-user feedback/issue reporting — users send issues to app operators, AI auto-classifies summary/urgency/category
- CLI: Command-line tool for deploying web storage and tunneling local services
CLI
Deploy your web application to Connect Base Web Storage with a single command.
Quick Start
# 1. Initialize (one-time setup)
npx connectbase init
# 2. Deploy
npm run deployThe init command will:
- Ask for your Public Key
- List existing web storages or create a new one automatically
- Create a
.connectbasercconfig file - Add
.connectbasercto.gitignore - Add a
deployscript topackage.json(includesbuildif available)
Commands
| Command | Description |
|---------|-------------|
| init | Interactive project setup (creates config, adds deploy script) |
| deploy <dir> | Deploy files to web storage |
| tunnel <port> | Expose a local service to the internet via WebSocket tunnel |
Manual Usage
If you prefer not to use init, you can pass options directly:
npx connectbase deploy ./dist -s <storage-id> -k <public-key>Options
| Option | Alias | Description |
|--------|-------|-------------|
| --storage <id> | -s | Storage ID |
| --public-key <key> | -k | API Key |
| --base-url <url> | -u | Custom server URL |
| --timeout <sec> | -t | Tunnel request timeout in seconds (tunnel only) |
| --max-body <MB> | | Tunnel max body size in MB (tunnel only) |
| --label <name> | | Auto-register the issued tunnel as an endpoint binding (tunnel only). Requires Secret Key. SDK callers can then use cb.endpoint.call(label, …) |
| --description <text> | | Endpoint binding description (only valid with --label) |
| --help | -h | Show help |
| --version | -v | Show version |
Tunnel
Expose a local server to the internet through a secure WebSocket tunnel. Useful for sharing local MCP servers, development servers, or any HTTP service.
# Expose local port 8084 to the internet
npx connectbase tunnel 8084 -k <public-key>
# With environment variable
export CONNECTBASE_PUBLIC_KEY=your-public-key
npx connectbase tunnel 8084
# For GPU servers or long-running tasks (e.g., image generation)
npx connectbase tunnel 7860 --timeout 300 --max-body 50The tunnel creates a public URL like https://tunnel.connectbase.world/<tunnel-id>/ that proxies all HTTP requests to your local service.
Plan-based limits: Timeout and body size are clamped to your plan's maximum:
| Plan | Max Timeout | Max Body | |------|-------------|----------| | Free | 60s | 10MB | | Starter | 120s | 25MB | | Pro | 300s | 50MB | | Business | 600s | 100MB |
Features:
- Per-tunnel timeout and body size configuration
- Automatic reconnection with exponential backoff
- Request/response logging in terminal
- Graceful shutdown with Ctrl+C
- No external dependencies (uses Node.js built-in modules)
Auto-register an endpoint binding (--label)
For workflows where you want the SDK to call your local model by a stable name
(cb.endpoint.call("comfyui-main", …)) instead of a random tunnel URL, pass
--label <name>. The CLI registers the issued tunnel_id as an endpoint binding
on the server, so your SDK only needs the Public Key.
# Start ComfyUI on port 8188, expose it as endpoint label "comfyui-main"
npx connectbase tunnel 8188 --label comfyui-main --description "ComfyUI on my desktop"Authentication uses your User Secret Key (cb_sk_*); the CLI calls the dual-auth
route POST /v1/apps/:appID/endpoints/cli. If the label already exists, the CLI
warns and keeps the tunnel running — update the binding to the new tunnel_id
from the console if needed.
Configuration File
The init command creates .connectbaserc automatically. You can also create it manually:
{
"publicKey": "your-public-key",
"storageId": "your-storage-id",
"deployDir": "./dist"
}Environment Variables
export CONNECTBASE_PUBLIC_KEY=your-public-key
export CONNECTBASE_STORAGE_ID=your-storage-id
npx connectbase deploy ./distRequirements
index.htmlmust exist in the root of the deploy directory- Supported file types:
.html,.css,.js,.json,.svg,.png,.jpg,.gif,.webp,.woff,.woff2,.ttf,.mp3,.mp4,.pdf,.wasm, etc.
SPA Mode
Web Storage supports SPA (Single Page Application) mode, which is enabled by default. When enabled, requests to non-existent paths return index.html instead of 404, allowing client-side routers (React Router, Vue Router, etc.) to handle routing.
You can toggle SPA mode in the Connect Base Console under Storage > Security Settings, or via the API:
# Disable SPA mode (for static sites)
curl -X PUT "https://api.connectbase.world/v1/apps/{appID}/storages/webs/{storageID}" \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"spa_mode": false}'Important: When using SPA mode, ensure all asset paths in your HTML are absolute (
/assets/...), not relative (./assets/...). For Vite projects, setbase: '/'invite.config.ts.
API Reference
Game Server
cb.game.config — Feature Opt-in (v3.1+, SDK 3.3.0+)
The game server's 7 features (matchqueue / leaderboard / entity / scripts /
voice / replay / spectator) are explicit opt-in per app as of v3.1
(2026-04-30). Disabled features return HTTP 403 feature_disabled.
New apps default to all-OFF; existing apps without a config row fall back to
all-ON for compatibility.
// Inspect current toggles
const cfg = await cb.game.config.get(appId)
// → { matchqueue_enabled, leaderboard_enabled, entity_enabled, scripts_enabled,
// voice_enabled, replay_enabled, spectator_enabled }
// Partial update — only the keys you send are applied; others are preserved.
await cb.game.config.set(appId, {
matchqueue_enabled: true,
leaderboard_enabled: true,
})
// Single-toggle convenience wrappers
await cb.game.config.enable(appId, 'matchqueue_enabled')
await cb.game.config.disable(appId, 'voice_enabled')The PATCH publishes a NATS invalidation so game-server caches drop the entry immediately (30s TTL is the safety net).
| HTTP code | Body error | Meaning | Client action |
|-----------|--------------|---------|---------------|
| 403 | feature_disabled | Feature is not enabled for this app | Toggle via console or cb.game.config.set(...) |
| 429 | cap_exceeded | Per-app cardinality cap reached | Remove old rows or ask the operator to raise the cap env |
| 402 | quota_exceeded | Plan limit reached on a write route | Upgrade plan |
See docs/game-server/OPT_IN.md for the full policy.
GameRoom
The main class for real-time game communication.
const gameClient = cb.game.createClient({
clientId: 'unique-player-id', // Required: Unique identifier for this player
gameServerUrl: 'wss://...', // Optional: Custom game server URL
autoReconnect: true, // Optional: Auto-reconnect on disconnect (default: true)
maxReconnectAttempts: 5, // Optional: Max reconnect attempts (default: 5)
reconnectInterval: 1000, // Optional: Base reconnect interval in ms (default: 1000)
connectionTimeout: 10000, // Optional: Connection timeout in ms (default: 10000)
})Properties
| Property | Type | Description |
|----------|------|-------------|
| roomId | string \| null | Current room ID |
| state | GameState \| null | Current game state |
| isConnected | boolean | Connection status |
| isOfflineMode | boolean | Offline mode status |
| latency | number | Current latency in ms |
| connectionState | ConnectionState | Detailed connection state |
Methods
connect(roomId?: string): Promise<void>
Connect to the game server. Optionally specify a room ID to join immediately.
await gameClient.connect()
// or
await gameClient.connect('existing-room-id')disconnect(): void
Disconnect from the game server.
gameClient.disconnect()createRoom(config?: GameRoomConfig): Promise<GameState>
Create a new game room.
const state = await gameClient.createRoom({
roomId: 'my-custom-room', // Optional: Custom room ID
categoryId: 'battle-royale', // Optional: Room category
maxPlayers: 100, // Optional: Max players (default: 100)
tickRate: 64, // Optional: Server tick rate (default: 64)
scriptName: 'main', // Optional (3.11.0+): Lua script attached to the room
// (uploaded+activated via console or POST /v1/game/:appID/scripts).
// Required for onTick / onPlayerJoin / onAction etc. to fire.
metadata: { map: 'forest' } // Optional: Custom metadata
})joinRoom(roomId: string, metadata?: Record<string, string>): Promise<GameState>
Join an existing room.
const state = await gameClient.joinRoom('room-id', {
team: 'blue',
displayName: 'Player1'
})leaveRoom(): Promise<void>
Leave the current room.
await gameClient.leaveRoom()sendAction(action: GameAction): void
Send a game action to the server.
gameClient.sendAction({
type: 'move',
data: { x: 100, y: 200 }
})
gameClient.sendAction({
type: 'attack',
data: { targetId: 'enemy-1', damage: 50 }
})sendChat(message: string): void
Send a chat message to the room.
gameClient.sendChat('Hello everyone!')requestState(): Promise<GameState>
Request the full current state from the server.
const state = await gameClient.requestState()listRooms(): Promise<GameRoomInfo[]>
List all available rooms.
const rooms = await gameClient.listRooms()
rooms.forEach(room => {
console.log(`${room.id}: ${room.playerCount}/${room.maxPlayers}`)
})ping(): Promise<number>
Measure round-trip time to the server.
const rtt = await gameClient.ping()
console.log(`Latency: ${rtt}ms`)Event Handlers
gameClient
.on('onConnect', () => {
// Called when connected to the server
})
.on('onDisconnect', (event: CloseEvent) => {
// Called when disconnected
})
.on('onStateUpdate', (state: GameState) => {
// Called when full state is received
})
.on('onDelta', (delta: GameDelta) => {
// Called for incremental state updates
// Use this for efficient state synchronization
})
.on('onPlayerJoined', (player: GamePlayer) => {
// Called when a player joins the room
})
.on('onPlayerLeft', (player: GamePlayer) => {
// Called when a player leaves the room
})
.on('onChat', (message: ChatMessage) => {
// Called when a chat message is received
})
.on('onError', (error: ErrorMessage) => {
// Called on errors
})
.on('onPong', (pong: PongMessage) => {
// Called when pong is received
})Offline Mode
Test your game logic locally without a server connection.
// Enable offline mode
gameClient.enableOfflineMode({
tickRate: 64,
initialState: {
players: {},
objects: []
},
simulatedPlayers: [
{ clientId: 'bot-1', joinedAt: Date.now(), metadata: { isBot: 'true' } }
]
})
// Update state directly
gameClient.setOfflineState('players.player-1.position', { x: 100, y: 200 })
// Add/remove simulated players
gameClient.addSimulatedPlayer({
clientId: 'bot-2',
joinedAt: Date.now(),
metadata: {}
})
gameClient.removeSimulatedPlayer('bot-2')
// Disable offline mode
gameClient.disableOfflineMode()Authentication
// ID/Password signup
const result = await cb.auth.signUpMember({
login_id: 'myuser123',
password: 'password123',
nickname: 'John'
})
// ID/Password login
const result = await cb.auth.signInMember({
login_id: 'myuser123',
password: 'password123'
})
// Guest login
const guest = await cb.auth.signInAsGuestMember()
// OAuth login (redirect - recommended)
await cb.oauth.signIn('google', 'https://myapp.com/oauth/callback')
// OAuth login (popup - COOP restrictions may apply)
const result = await cb.oauth.signInWithPopup('google', 'https://myapp.com/oauth/callback')
// Sign out
await cb.auth.signOut()Database
// Query data
const { data, total_count } = await cb.database.getData('table-id', {
where: { status: 'active' },
limit: 10
})
// Query with field selection (Projection) - improves response speed
const { data } = await cb.database.getData('table-id', {
select: ['id', 'name', 'thumbnail'], // Only return these fields
limit: 20
})
// Exclude specific fields (e.g., large HTML/CSS content)
const { data } = await cb.database.getData('table-id', {
exclude: ['html_content', 'css_content'],
limit: 20
})
// Insert data — returns the created DataItem (id + data + created_at + updated_at)
const newItem = await cb.database.createData('table-id', {
data: { name: 'John', email: '[email protected]' }
})
console.log(newItem.id) // use immediately for navigation / cache updates
// Bulk insert — returns { created: DataItem[], total, success, failed? }
const bulk = await cb.database.createMany('table-id', [
{ data: { name: 'User1' } },
{ data: { name: 'User2' } }
])
// Update data — returns the updated DataItem with merged fields
const updated = await cb.database.updateData('table-id', 'data-id', {
data: { name: 'Jane' }
})
// Delete data
await cb.database.deleteData('table-id', 'data-id')Aggregation (MongoDB-style Pipeline)
const result = await cb.database.aggregate('table-id', [
{ match: { status: 'active' } },
{ group: { _id: '$category', total: { $sum: '$price' }, count: { $sum: 1 } } },
{ sort: { total: -1 } },
{ limit: 10 }
])
console.log(result.results) // [{ _id: 'electronics', total: 5000, count: 12 }, ...]Full-Text Search
// Fuzzy search with highlighting
const results = await cb.database.search('table-id', 'smrt phone', ['name', 'description'], {
fuzzy: true,
fuzzy_distance: 2,
highlight: true,
limit: 10
})
results.results.forEach(item => {
console.log(item.data.name, item.score, item.highlights)
})
// Autocomplete
const suggestions = await cb.database.autocomplete('table-id', 'sma', 'name', { limit: 5 })Geo Queries
// Find locations within 5km radius
const nearby = await cb.database.geoQuery('table-id', 'location', {
near: {
center: { latitude: 37.5665, longitude: 126.9780 },
max_distance: 5000
}
}, { limit: 20 })
nearby.results.forEach(place => {
console.log(place.data.name, `${place.distance}m away`)
})Batch & Transactions
// Batch: atomic multi-table operations
await cb.database.batch([
{ type: 'create', table: 'orders', data: { product: 'A', qty: 1 } },
{ type: 'update', table: 'inventory', doc_id: 'item-a', operators: {
qty: { type: 'increment', value: -1 }
}},
{ type: 'update', table: 'stats', doc_id: 'daily', operators: {
order_count: { type: 'increment', value: 1 },
last_order: { type: 'serverTimestamp' }
}}
])
// Transaction: read-then-write with ACID guarantees
await cb.database.transaction(
[{ table: 'accounts', doc_id: 'user-1', alias: 'sender' }],
[{ type: 'update', table: 'accounts', doc_id: 'user-1', operators: {
balance: { type: 'increment', value: -100 }
}}]
)Populate (Relation Query / JOIN)
// Query with related data populated
const posts = await cb.database.getDataWithPopulate('posts-table', {
limit: 10,
populate: [
{ field: 'author_id', from: 'users', as: 'author', select: ['name', 'avatar'] },
{ field: 'id', from: 'comments', as: 'comments', limit: 5, orderBy: 'created_at', order: 'desc' }
]
})Security Rules (RLS)
// Set row-level security rules
await cb.database.createSecurityRule('app-id', {
table_name: 'posts',
rules: {
read: 'true', // Anyone can read
create: 'auth.member_id != null', // Only authenticated users
update: 'auth.member_id == data.author_id', // Only author
delete: 'auth.member_id == data.author_id'
}
})
// List rules
const rules = await cb.database.listSecurityRules('app-id')Indexes
// Create unique index
await cb.database.createIndex('app-id', 'table-id', {
name: 'idx_email_unique',
fields: ['email'],
unique: true
})
// Analyze and get recommendations
const analysis = await cb.database.analyzeIndexes('app-id', 'table-id')
analysis.recommendations.forEach(rec => {
console.log(`Recommended: ${rec.fields.join(', ')} — ${rec.reason}`)
})Triggers
// Auto-execute function on data change
await cb.database.createTrigger('app-id', {
name: 'on-order-created',
table_name: 'orders',
event: 'create',
handler_type: 'function',
handler_id: 'send-notification-fn-id'
})Lifecycle (TTL / Retention)
// Auto-delete expired sessions
await cb.database.setTTL('app-id', {
table_name: 'sessions',
field: 'expires_at',
enabled: true
})
// Archive old logs after 90 days
await cb.database.setRetentionPolicy('app-id', {
table_name: 'logs',
retention_days: 90,
action: 'archive',
archive_table: 'archived_logs',
enabled: true
})Storage
// 파일 업로드 (UUID 기반 URL - 매번 변경됨)
const result = await cb.storage.uploadFile('storage-id', file)
console.log(result.url)
// 특정 폴더에 업로드
const result = await cb.storage.uploadFile('storage-id', file, 'folder-id')
// 경로 기반 업로드 (고정 URL - Firebase Storage 스타일)
// 같은 경로에 다시 업로드하면 URL이 유지된 채로 파일만 교체
const result = await cb.storage.uploadByPath(
'storage-id',
'/profiles/user123/avatar.png',
file
)
console.log(result.url) // 항상 동일한 URL
// 경로로 파일 조회
const file = await cb.storage.getByPath('storage-id', '/profiles/user123/avatar.png')
// 경로로 URL만 가져오기 (없으면 null)
const url = await cb.storage.getUrlByPath('storage-id', '/profiles/user123/avatar.png')
// 파일 목록 조회
const files = await cb.storage.getFiles('storage-id')
// 파일 삭제
await cb.storage.deleteFile('storage-id', 'file-id')
// 페이지 메타 설정 (SEO / OG 태그 - 웹 스토리지용)
await cb.storage.setPageMeta('web-storage-id', {
path: '/products/123',
title: '최신 스마트폰',
description: '최고의 성능, 최저가 보장',
image: 'https://example.com/product.jpg',
og_type: 'product',
json_ld: JSON.stringify({ "@context": "https://schema.org", "@type": "Product", "name": "스마트폰" }),
robots_noindex: false // true면 검색 결과에서 제외
})
// 여러 페이지 일괄 등록
await cb.storage.batchSetPageMeta('web-storage-id', {
pages: [
{ path: '/products/1', title: '상품 1', description: '설명 1' },
{ path: '/products/2', title: '상품 2', description: '설명 2' },
]
})
// 페이지 메타 조회/삭제
const { pages } = await cb.storage.listPageMetas('web-storage-id')
await cb.storage.deletePageMeta('web-storage-id', '/products/123')Realtime
// Connect to WebSocket
await cb.realtime.connect()
// Subscribe to a category
const subscription = await cb.realtime.subscribe('chat-room')
// Listen for messages
subscription.onMessage((message) => {
console.log('New message:', message.data)
})
// Send message
await subscription.send({ text: 'Hello!' })
// Unsubscribe
await subscription.unsubscribe()
// Disconnect
await cb.realtime.disconnect()Presence / Typing
Presence(온라인 상태) 와 typing(입력 중 표시) 은 cb.realtime.* 가 단일 SoT 입니다.
v2.0.0 에서 cb.database.setPresence / subscribePresence 는 제거되었습니다.
v1.x 에서 마이그레이션은 MIGRATION-v2.md 참고.
// 본인 온라인 상태 publish
await cb.realtime.setPresence('online', { device: 'web', metadata: { nickname: '홍길동' } })
// 다른 사용자 상태 구독
const unsub = await cb.realtime.subscribePresence('user-id', (info) => {
console.log(info.userId, info.status, info.eventType) // join | leave | update
})
// 룸 단위 typing indicator
await cb.realtime.startTyping('room-id')
await cb.realtime.stopTyping('room-id')
const unsubTyping = await cb.realtime.onTypingChange('room-id', (typing) => {
console.log(typing.users) // 현재 입력 중인 사용자 ID 목록
})AI Streaming
Real-time AI text generation using Gemini API through WebSocket.
// Connect first
await cb.realtime.connect()
// Start AI streaming
const session = await cb.realtime.stream(
[
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: 'Explain quantum computing in simple terms.' }
],
{
onToken: (token, index) => {
// Called for each generated token
process.stdout.write(token)
},
onDone: (result) => {
// Called when generation completes
console.log('\n\nFull text:', result.fullText)
console.log('Total tokens:', result.totalTokens)
console.log('Duration:', result.duration, 'ms')
},
onError: (error) => {
console.error('Stream error:', error.message)
}
},
{
model: 'gemini-2.0-flash', // Optional: default is gemini-2.0-flash
temperature: 0.7, // Optional: 0.0-2.0, default 0.7
maxTokens: 1000 // Optional: max output tokens
}
)
// Stop streaming early if needed
await session.stop()Stream Options:
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| provider | 'gemini' | 'gemini' | AI provider |
| model | string | 'gemini-2.0-flash' | Model name |
| system | string | - | System prompt |
| temperature | number | 0.7 | Creativity (0.0-2.0) |
| maxTokens | number | - | Max output tokens |
| sessionId | string | auto | Session tracking ID |
| metadata | object | - | Custom metadata |
Stream Result (onDone):
| Field | Type | Description |
|-------|------|-------------|
| sessionId | string | Session ID |
| fullText | string | Complete generated text |
| totalTokens | number | Total tokens generated |
| promptTokens | number | Input prompt tokens |
| duration | number | Generation time in ms |
Endpoint (Local Model Tunnel)
cb.endpoint.* is a dumb pipe to your own GPU/model server running behind a
ConnectBase tunnel. ConnectBase doesn't know your model, payload, or response
shape — it routes a cb_pk_* call by label to the registered tunnel and forwards
the body and headers as-is.
Setup: run connectbase tunnel <port> --label <name> once on the machine
hosting the model (see Tunnel) — that registers the binding. Then any
client with the app's Public Key can call it.
cb.endpoint.call(label, init): Promise<Response>
fetch()-compatible signature. Returns the raw Response — read the body as
JSON, text, or stream as needed.
const cb = new ConnectBase({ publicKey: 'cb_pk_...' })
// ComfyUI prompt graph
const res = await cb.endpoint.call('comfyui-main', {
method: 'POST',
path: '/prompt',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: { /* ComfyUI node graph */ } }),
})
const data = await res.json()// Streaming response (SSE / chunked) — vLLM chat completions
const res = await cb.endpoint.call('vllm-local', {
method: 'POST',
path: '/v1/chat/completions',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ stream: true, messages: [/* { role, content } */] }),
})
if (!res.body) throw new Error('no stream')
const reader = res.body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) break
// value is a Uint8Array — decode and process chunk
}// Cancel an in-flight request
const ctrl = new AbortController()
setTimeout(() => ctrl.abort(), 30_000)
await cb.endpoint.call('hunyuan-laptop', {
method: 'POST',
path: '/generate',
signal: ctrl.signal,
body: JSON.stringify({ /* model input */ }),
})EndpointCallInit
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| path | string | yes | Path on your model server, must start with / (e.g. /prompt, /v1/chat/completions) |
| method | string | no (GET) | HTTP method |
| headers | HeadersInit | no | Extra request headers; X-Public-Key is auto-injected unless you set it |
| body | BodyInit \| null | no | Request body — string, Blob, ArrayBuffer, FormData, or ReadableStream |
| signal | AbortSignal | no | Abort signal for cancellation |
The SDK assembles the URL as ${baseUrl}/v1/proxy/${label}${path} and forwards
the request. Because the response is the raw fetch Response, streaming
formats (SSE, chunked, NDJSON) work out of the box.
cb.endpoint.pollUntil<T>(label, init, predicate, opts?): Promise<T>
One-line "submit job → poll for result" pattern. Repeatedly calls the same
endpoint until predicate returns a value. Designed for ComfyUI /history/{id},
A1111 /sdapi/v1/progress, or any custom queue API.
Behavior:
- Calls
cb.endpoint.call(label, init)and passes the parsed body topredicate - Returns
undefinedfrompredicate→ waitintervalMsand retry - Returns a value from
predicate→ resolve immediately with that value - HTTP
5xx/ network error → retry. HTTP4xx→ reject (job-level error) timeoutMsexceeded orsignalaborted → reject
type Hist = Record<
string,
{ outputs: Record<string, { images?: { filename: string }[] }> }
>
const filename = await cb.endpoint.pollUntil<string>(
'comfyui-main',
{ path: `/history/${promptId}` },
(data: Hist) => {
const entry = data[promptId]
if (!entry) return undefined // still queued
for (const out of Object.values(entry.outputs)) {
const img = out.images?.[0]
if (img) return img.filename
}
return undefined
},
{ intervalMs: 1000, timeoutMs: 5 * 60_000 },
)PollUntilOptions
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| intervalMs | number | 1500 | Poll interval in ms |
| timeoutMs | number | 300000 (5 min) | Total timeout in ms — reject if exceeded |
| parse | 'json' \| 'text' \| 'none' | 'json' | Body parser. 'json' falls back to undefined on parse error |
| signal | AbortSignal | — | External cancel signal — reject immediately on abort |
cb.endpoint.url(label, path): string
Returns the assembled call URL (${baseUrl}/v1/proxy/${label}${path}) for
URL-passing scenarios where you control the request and can attach the
X-Public-Key header yourself.
⚠️ Browser-native APIs that cannot set custom headers will fail with 401.
ConnectBase's proxy requires X-Public-Key on every call (header-only — no
?api_key= fallback), so <img src>, new Image(), native WebSocket,
<script src>, EventSource, etc. cannot authenticate through this URL.
Use cb.endpoint.call() instead for those cases:
// ✅ Render an image: download via call(), then convert to a blob URL
const res = await cb.endpoint.call('comfyui-main', {
path: `/view?filename=${encodeURIComponent(name)}`,
})
img.src = URL.createObjectURL(await res.blob())
// ...later: URL.revokeObjectURL(img.src)For permanent images (works across CDN, survives tunnel restarts), upload the
blob to cb.storage and use saved.url — see
examples/ai-image-generator/.
When cb.endpoint.url() IS the right tool:
- Logging / debugging the resolved tunnel URL
- Passing the URL to a backend service or worker that will make the call with proper headers
- Building a
RequestInfofor a customfetch()wrapper (you control headers)
console.log(cb.endpoint.url('comfyui-main', '/prompt'))
// → https://api.connectbase.world/v1/proxy/comfyui-main/prompt
// Hand the URL to a Service Worker that injects X-Public-Key
sw.postMessage({ url: cb.endpoint.url('comfyui-main', '/prompt'), key: PK })Push Notifications
// Register a device (FCM for Android, APNS for iOS)
const device = await cb.push.registerDevice({
device_token: 'fcm-token-or-apns-token',
platform: 'android', // 'android' | 'ios' | 'web'
device_name: 'Galaxy S24'
})
// Subscribe the device to a topic (deviceToken is required)
await cb.push.subscribeTopic(device.device_token, 'news')
// Unsubscribe the device from a topic
await cb.push.unsubscribeTopic(device.device_token, 'news')
// Web Push (browsers)
const vapidKey = await cb.push.getVAPIDPublicKey()
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: vapidKey.public_key
})
await cb.push.registerWebPush(subscription)WebRTC
// Public Key/JWT 유효성 사전 검증
const result = await cb.webrtc.validate()
if (result.valid) {
console.log('인증 성공:', result.app_id)
}
// 로컬 미디어 스트림 가져오기
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })
// WebRTC 연결
await cb.webrtc.connect({
roomId: 'live:room-1',
isBroadcaster: true,
localStream: stream
})
// 원격 스트림 수신
cb.webrtc.onRemoteStream((peerId, remoteStream) => {
videoElement.srcObject = remoteStream
})
// 연결 해제
cb.webrtc.disconnect()Payments & Subscriptions
// Create a subscription
const subscription = await cb.subscription.create({
planId: 'premium-monthly',
billingKeyId: 'billing-key-1'
})
// Check subscription status
const status = await cb.subscription.getStatus()
// Cancel subscription
await cb.subscription.cancel()Support (End-user Issue Reporting)
End-user 가 앱 운영자에게 직접 버그·질문·요청을 발행하는 채널. 운영자 콘솔의 inbox 에 들어가며, AI 가 자동으로 요약·긴급도·카테고리를 분류한다 (운영자가 AI config 등록 시).
// 로그인 사용자 (AppMember JWT 자동 첨부)
await cb.support.reportIssue({
title: "결제 화면이 멈춰요",
body: "결제 버튼 클릭 후 로딩이 끝나지 않습니다.",
category: "bug", // bug | question | feature_request | incident | other
metadata: { pageUrl: window.location.href }
})
// 익명 발행 + reCAPTCHA v3 (운영자가 RECAPTCHA_SECRET 설정한 경우 권장)
const recaptchaToken = await grecaptcha.execute(SITE_KEY, { action: 'report_issue' })
await cb.support.reportIssue({
title: "...",
body: "...",
anonymousEmail: "[email protected]", // 회신 받을 이메일 (선택)
recaptchaToken,
})응답: { id, status: 'open', created_at } (보안상 최소 정보만).
발행자가 결과를 조회하는 채널은 후속 plan 에서 추가될 예정 — 현재는 운영자가 외부 webhook(이메일/Slack 등)으로 회신하는 방식 권장.
Types
GameState
interface GameState {
roomId: string
state: Record<string, unknown> // Your game state
version: number
serverTime: number
tickRate: number
players: GamePlayer[]
}GameDelta
interface GameDelta {
fromVersion: number
toVersion: number
changes: Array<{
path: string
operation: 'set' | 'delete'
value?: unknown
}>
tick: number
}GamePlayer
interface GamePlayer {
clientId: string
joinedAt: number
metadata?: Record<string, string>
}ConnectionState
interface ConnectionState {
status: 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error' | 'offline'
reconnectAttempt: number
lastError?: Error
latency: number
}Error Handling
import ConnectBase, { ApiError, AuthError } from 'connectbase-client'
try {
await cb.auth.signInMember({ login_id, password })
} catch (error) {
if (error instanceof ApiError) {
// HTTP 응답 기반 에러: status/code/details 로 분기 가능
if (error.statusCode === 429) {
const details = error.details as { retry_after_seconds?: number } | undefined
const retryAfter = details?.retry_after_seconds
// ...
}
} else if (error instanceof AuthError) {
// refresh 실패/토큰 만료
}
}
// Game API 는 별도 이벤트 핸들러도 지원
gameClient.on('onError', (error) => {
console.error('Game error:', error.message)
})전역 에러 관찰자 (v1.9.0+)
ConnectBase 초기화 시 onError 옵션을 주면 모든 ApiError / AuthError 가 한 곳으로 모입니다. Sentry/Datadog 등 관측성 툴과 연결하기 쉽습니다.
const cb = new ConnectBase({
publicKey: 'cb_pk_...',
onError: (error) => {
Sentry.captureException(error)
},
})요청 타임아웃 (v1.9.0+)
기본 30초 타임아웃이 모든 HTTP 호출에 적용됩니다. requestTimeoutMs 로 전역 기본값을 바꾸거나, 0 이하 값을 주면 비활성화할 수 있습니다.
const cb = new ConnectBase({
publicKey: 'cb_pk_...',
requestTimeoutMs: 60000, // 60s
})Best Practices
State Synchronization
Use delta updates for efficient state synchronization:
gameClient.on('onDelta', (delta) => {
// Apply only the changes instead of replacing entire state
for (const change of delta.changes) {
applyChange(localState, change.path, change.operation, change.value)
}
})Reconnection Handling
gameClient.on('onDisconnect', (event) => {
if (event.code !== 1000) {
// Show reconnecting UI
showReconnectingMessage()
}
})
gameClient.on('onConnect', () => {
// Reconnected - request full state
gameClient.requestState()
hideReconnectingMessage()
})Latency Compensation
// Measure latency periodically
setInterval(async () => {
const rtt = await gameClient.ping()
// Adjust client-side prediction based on latency
updatePredictionOffset(rtt / 2)
}, 5000)Examples
Simple Multiplayer Game
import ConnectBase from 'connectbase-client'
const cb = new ConnectBase({ publicKey: 'your-public-key' })
const game = cb.game.createClient({ clientId: `player-${Date.now()}` })
// Local player state
let localPlayer = { x: 0, y: 0 }
game
.on('onConnect', () => console.log('Connected'))
.on('onStateUpdate', (state) => {
// Render all players
renderPlayers(state.state.players)
})
.on('onDelta', (delta) => {
// Efficient incremental updates
for (const change of delta.changes) {
updatePlayer(change.path, change.value)
}
})
// Connect and create room
await game.connect()
await game.createRoom({ maxPlayers: 8 })
// Game loop
function gameLoop() {
// Read input
const input = getPlayerInput()
// Send action
if (input.moved) {
game.sendAction({
type: 'move',
data: { x: input.x, y: input.y }
})
}
requestAnimationFrame(gameLoop)
}
gameLoop()Chat Application
const game = cb.game.createClient({ clientId: userId })
game.on('onChat', (message) => {
displayMessage(message.senderId, message.message, message.timestamp)
})
await game.connect()
await game.joinRoom('general-chat')
// Send message
chatInput.addEventListener('submit', () => {
game.sendChat(chatInput.value)
chatInput.value = ''
})License
MIT
