npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

connectbase-client

v3.12.0

Published

Connect Base JavaScript/TypeScript SDK for browser and Node.js

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-client

Key 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 deploy

The init command will:

  • Ask for your Public Key
  • List existing web storages or create a new one automatically
  • Create a .connectbaserc config file
  • Add .connectbaserc to .gitignore
  • Add a deploy script to package.json (includes build if 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 50

The 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 ./dist

Requirements

  • index.html must 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, set base: '/' in vite.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 to predicate
  • Returns undefined from predicate → wait intervalMs and retry
  • Returns a value from predicate → resolve immediately with that value
  • HTTP 5xx / network error → retry. HTTP 4xx → reject (job-level error)
  • timeoutMs exceeded or signal aborted → 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 RequestInfo for a custom fetch() 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