RTDB (Real-Time Database)
The Cloudillo RTDB provides a Firebase-like real-time database with TypeScript support.
RTDB vs CRDT
RTDB is best for structured data with queries (todos, settings, lists). For collaborative document editing where multiple users edit simultaneously, see CRDT. Compare both in Data Storage & Access.
Installation
pnpm add @cloudillo/rtdbQuick Start
import { getAppBus } from '@cloudillo/core'
import { RtdbClient } from '@cloudillo/rtdb'
import { getRtdbUrl } from '@cloudillo/core'
// Initialize Cloudillo
const bus = getAppBus()
await bus.init('my-app')
// Create RTDB client
const rtdb = new RtdbClient({
dbId: 'my-database-id',
auth: {
getToken: () => bus.accessToken
},
serverUrl: getRtdbUrl(bus.idTag!, 'my-database-id', bus.accessToken!)
})
// Connect to the database
await rtdb.connect()
// Get collection reference
const todos = rtdb.collection('todos')
// Create document
const batch = rtdb.batch()
batch.create(todos, {
title: 'Learn Cloudillo RTDB',
completed: false,
createdAt: Date.now()
})
await batch.commit()
// Subscribe to changes
todos.onSnapshot((snapshot) => {
console.log('Todos:', snapshot.docs.map(doc => doc.data()))
})RtdbClient Constructor
interface RtdbClientOptions {
dbId: string // Database/file ID
auth: {
getToken: () => string | undefined | Promise<string | undefined>
}
serverUrl: string // WebSocket URL
options?: {
enableCache?: boolean // Default: false
reconnect?: boolean // Default: true
reconnectDelay?: number // Default: 1000ms
maxReconnectDelay?: number // Default: 30000ms
debug?: boolean // Default: false
}
}Example with all options:
import { RtdbClient } from '@cloudillo/rtdb'
import { getRtdbUrl } from '@cloudillo/core'
const rtdb = new RtdbClient({
dbId: 'my-database-id',
auth: {
// Token provider function - called when connection needs auth
getToken: async () => {
// Can be sync or async
return bus.accessToken
}
},
serverUrl: getRtdbUrl(bus.idTag!, 'my-database-id', bus.accessToken!),
options: {
enableCache: true, // Enable local caching
reconnect: true, // Auto-reconnect on disconnect
reconnectDelay: 1000, // Initial reconnect delay
maxReconnectDelay: 30000, // Max reconnect delay (exponential backoff)
debug: false // Enable debug logging
}
})Connection Management
// Connect to database
await rtdb.connect()
// Disconnect
await rtdb.disconnect()
// Check connection status
if (rtdb.isConnected()) {
console.log('Connected')
}
// Diagnostics
console.log('Pending requests:', rtdb.getPendingRequests())
console.log('Active subscriptions:', rtdb.getActiveSubscriptions())Core Concepts
Collections
Collections are groups of documents, similar to tables in SQL.
const users = rtdb.collection('users')
const posts = rtdb.collection('posts')
const comments = rtdb.collection('comments')
// Typed collections
interface Todo {
title: string
completed: boolean
createdAt: number
}
const todos = rtdb.collection<Todo>('todos')Documents
Documents are individual records accessed by path.
// Reference a document by path
const userDoc = rtdb.ref('users/alice')
// Get document data
const snapshot = await userDoc.get()
if (snapshot.exists) {
console.log(snapshot.data())
}CRUD Operations
Create
Use batch operations to create documents:
const todos = rtdb.collection('todos')
const batch = rtdb.batch()
// Create with auto-generated ID
batch.create(todos, {
title: 'New task',
completed: false
})
// Create with ref for tracking
batch.create(todos, {
title: 'Another task',
completed: false
}, { ref: 'task-ref' })
// Commit returns results with IDs
const results = await batch.commit()
console.log('Created IDs:', results.map(r => r.id))Read
// Get single document
const doc = rtdb.ref('todos/task_123')
const snapshot = await doc.get()
if (snapshot.exists) {
console.log(snapshot.data())
}
// Query collection
const todos = rtdb.collection('todos')
const results = await todos.get()
results.docs.forEach(doc => {
console.log(doc.id, doc.data())
})Update
const batch = rtdb.batch()
const todoRef = rtdb.ref('todos/task_123')
// Partial update
batch.update(todoRef, {
completed: true
})
await batch.commit()Delete
const batch = rtdb.batch()
const todoRef = rtdb.ref('todos/task_123')
batch.delete(todoRef)
await batch.commit()Queries
Query Options
interface QueryOptions {
filter?: {
equals?: Record<string, any>
}
sort?: Array<{ field: string; ascending: boolean }>
limit?: number
offset?: number
}Filtering
const todos = rtdb.collection('todos')
// Filter by field value
const incomplete = await todos.query({
filter: {
equals: { completed: false }
}
})Sorting
// Sort ascending
const sorted = await todos.query({
sort: [{ field: 'createdAt', ascending: true }]
})
// Sort descending
const newest = await todos.query({
sort: [{ field: 'createdAt', ascending: false }]
})
// Multiple sorts
const prioritized = await todos.query({
sort: [
{ field: 'priority', ascending: false },
{ field: 'createdAt', ascending: true }
]
})Pagination
// Limit results
const first10 = await todos.query({
limit: 10
})
// Pagination with offset
const page2 = await todos.query({
limit: 20,
offset: 20
})Combined Queries
const results = await todos.query({
filter: {
equals: { completed: false }
},
sort: [{ field: 'createdAt', ascending: false }],
limit: 10
})Real-Time Subscriptions
Collection Subscriptions
const todos = rtdb.collection('todos')
// Subscribe to all documents
const unsubscribe = todos.onSnapshot((snapshot) => {
console.log('Total todos:', snapshot.size)
snapshot.docs.forEach(doc => {
console.log(doc.id, doc.data())
})
// Track changes
const changes = snapshot.docChanges()
changes.forEach(change => {
switch (change.type) {
case 'added':
console.log('New:', change.doc.id)
break
case 'modified':
console.log('Updated:', change.doc.id)
break
case 'removed':
console.log('Deleted:', change.doc.id)
break
}
})
})
// Unsubscribe later
unsubscribe()Document Subscriptions
const todoRef = rtdb.ref('todos/task_123')
const unsubscribe = todoRef.onSnapshot((snapshot) => {
if (snapshot.exists) {
console.log('Todo updated:', snapshot.data())
} else {
console.log('Todo deleted')
}
})Filtered Subscriptions
const unsubscribe = todos.subscribe({
filter: {
equals: { completed: false }
}
}, (snapshot) => {
console.log('Incomplete todos:', snapshot.size)
})Batch Operations
Perform multiple operations atomically:
const todos = rtdb.collection('todos')
const batch = rtdb.batch()
// Create multiple
batch.create(todos, { title: 'Task 1', completed: false })
batch.create(todos, { title: 'Task 2', completed: false })
// Update existing
batch.update(rtdb.ref('todos/task_123'), { completed: true })
// Delete
batch.delete(rtdb.ref('todos/task_456'))
// Commit all operations atomically
const results = await batch.commit()
console.log('Batch results:', results)BatchResult:
interface BatchResult {
ref?: string // Reference ID if provided
id?: string // Generated document ID
}Indexes
Create indexes for efficient queries:
// Create index on a field
await rtdb.createIndex('todos', 'createdAt')
await rtdb.createIndex('todos', 'completed')TypeScript Support
Full type safety with generics:
interface Todo {
title: string
completed: boolean
createdAt: number
tags?: string[]
}
const todos = rtdb.collection<Todo>('todos')
// TypeScript knows the shape
const snapshot = await todos.get()
snapshot.docs.forEach(doc => {
const data = doc.data()
console.log(data.title) // string
console.log(data.completed) // boolean
// @ts-error: Property 'invalid' does not exist
// console.log(data.invalid)
})React Integration
import { useEffect, useState } from 'react'
import { useAuth } from '@cloudillo/react'
import { RtdbClient } from '@cloudillo/rtdb'
import { getRtdbUrl } from '@cloudillo/core'
interface Todo {
title: string
completed: boolean
}
function TodoList({ dbId }: { dbId: string }) {
const [auth] = useAuth()
const [todos, setTodos] = useState<Todo[]>([])
const [rtdb, setRtdb] = useState<RtdbClient | null>(null)
// Initialize RTDB client
useEffect(() => {
if (!auth?.token || !auth?.idTag) return
const client = new RtdbClient({
dbId,
auth: { getToken: () => auth.token },
serverUrl: getRtdbUrl(auth.idTag, dbId, auth.token!)
})
setRtdb(client)
return () => {
client.disconnect()
}
}, [auth?.token, auth?.idTag, dbId])
// Subscribe to todos
useEffect(() => {
if (!rtdb) return
const todos = rtdb.collection<Todo>('todos')
const unsubscribe = todos.onSnapshot((snapshot) => {
setTodos(snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
})))
})
return () => unsubscribe()
}, [rtdb])
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
)
}Error Handling
The RTDB client provides typed errors:
import {
RtdbError,
ConnectionError,
AuthError,
PermissionError,
NotFoundError,
ValidationError,
TimeoutError
} from '@cloudillo/rtdb'
try {
await rtdb.connect()
} catch (error) {
if (error instanceof ConnectionError) {
console.log('Connection failed:', error.message)
} else if (error instanceof AuthError) {
console.log('Authentication failed:', error.message)
} else if (error instanceof PermissionError) {
console.log('Permission denied:', error.message)
} else if (error instanceof NotFoundError) {
console.log('Not found:', error.message)
} else if (error instanceof TimeoutError) {
console.log('Request timed out:', error.message)
}
}Best Practices
1. Use Connection Management
// Initialize once, reuse
const rtdb = new RtdbClient({ ... })
await rtdb.connect()
// Use throughout your app
const todos = rtdb.collection('todos')2. Clean Up Subscriptions
// Always unsubscribe to prevent memory leaks
useEffect(() => {
const unsubscribe = todos.onSnapshot(callback)
return () => unsubscribe()
}, [])3. Use Batch for Multiple Operations
// Good: Atomic batch operation
const batch = rtdb.batch()
batch.create(todos, { title: 'Task 1' })
batch.create(todos, { title: 'Task 2' })
await batch.commit()
// Avoid: Multiple separate requests
// await create({ title: 'Task 1' })
// await create({ title: 'Task 2' })
4. Use Typed Collections
// Define your types
interface Todo {
title: string
completed: boolean
}
// Get type safety
const todos = rtdb.collection<Todo>('todos')5. Handle Connection State
// Check connection before operations
if (!rtdb.isConnected()) {
await rtdb.connect()
}
// Handle reconnection in UI
const [connected, setConnected] = useState(false)See Also
- @cloudillo/core - Core SDK
- @cloudillo/react - React integration
- Data Storage & Access - RTDB vs CRDT comparison