connectbase-client
v0.6.38
Published
Connect Base JavaScript/TypeScript SDK for browser and Node.js
Downloads
3,494
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-clientAPI Key Types
Connect Base provides two types of API 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_API_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 API 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({
apiKey: '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)
- 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-client init
# 2. Deploy
npm run deployThe init command will:
- Ask for your API 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-client deploy ./dist -s <storage-id> -k <api-key>Options
| Option | Alias | Description |
|--------|-------|-------------|
| --storage <id> | -s | Storage ID |
| --api-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) |
| --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-client tunnel 8084 -k <api-key>
# With environment variable
export CONNECTBASE_API_KEY=your-api-key
npx connectbase-client tunnel 8084
# For GPU servers or long-running tasks (e.g., image generation)
npx connectbase-client 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)
Configuration File
The init command creates .connectbaserc automatically. You can also create it manually:
{
"apiKey": "your-api-key",
"storageId": "your-storage-id",
"deployDir": "./dist"
}Environment Variables
export CONNECTBASE_API_KEY=your-api-key
export CONNECTBASE_STORAGE_ID=your-storage-id
npx connectbase-client 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
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: 10)
tickRate: 64, // Optional: Server tick rate (default: 64)
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 } = 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
const newItem = await cb.database.createData('table-id', {
data: { name: 'John', email: '[email protected]' }
})
// Update data
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()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 |
Push Notifications
// Register for push notifications
await cb.push.register({
token: 'fcm-token-or-apns-token',
platform: 'android' // or 'ios', 'web'
})
// Subscribe to topics
await cb.push.subscribeToTopic('news')
// Unsubscribe from topic
await cb.push.unsubscribeFromTopic('news')WebRTC
// API 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()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
try {
await gameClient.connect()
} catch (error) {
if (error instanceof Error) {
console.error('Connection failed:', error.message)
}
}
// Or use event handlers
gameClient.on('onError', (error) => {
console.error('Game error:', error.message)
})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({ apiKey: 'your-api-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
