RTDB (Real-Time Database)

Real-Time Database

The Cloudillo RTDB provides a Firebase-like real-time database with TypeScript support.

Installation

pnpm add @cloudillo/rtdb

Quick Start

import * as cloudillo from '@cloudillo/base'
import { RtdbClient } from '@cloudillo/rtdb'

// Initialize Cloudillo
await cloudillo.init('my-app')

// Create RTDB client
const rtdb = new RtdbClient({
  fileId: 'my-database-file-id',
  token: cloudillo.accessToken,
  url: 'wss://your-server.com/ws/rtdb'
})

// Get collection reference
const todos = rtdb.collection('todos')

// Create document
await todos.create({
  title: 'Learn Cloudillo RTDB',
  completed: false,
  createdAt: Date.now()
})

// Subscribe to changes
todos.onSnapshot((snapshot) => {
  console.log('Todos:', snapshot)
})

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')

Documents

Documents are individual records with unique IDs.

// Create with auto-generated ID
const doc = await users.create({
  name: 'Alice',
  email: 'alice@example.com'
})

console.log(doc.id) // Auto-generated ID

// Create with custom ID
await users.doc('user_alice').set({
  name: 'Alice',
  email: 'alice@example.com'
})

CRUD Operations

Create

// Auto-generated ID
const newDoc = await todos.create({
  title: 'New task',
  completed: false
})

// Custom ID
await todos.doc('task_123').set({
  title: 'Specific task',
  completed: false
})

Read

// Get single document
const doc = await todos.doc('task_123').get()
console.log(doc.data())

// Get all documents
const allTodos = await todos.get()

// Get with query
const incompleteTodos = await todos
  .where('completed', '==', false)
  .get()

Update

// Partial update
await todos.doc('task_123').update({
  completed: true
})

// Full replacement
await todos.doc('task_123').set({
  title: 'Updated title',
  completed: true,
  updatedAt: Date.now()
})

Delete

await todos.doc('task_123').delete()

Queries

where(field, operator, value)

Filter documents by field values.

Operators:

  • == - Equal
  • != - Not equal
  • > - Greater than
  • >= - Greater than or equal
  • < - Less than
  • <= - Less than or equal
  • in - Value in array
  • contains - Array contains value
// Equal
const activeTodos = await todos
  .where('completed', '==', false)
  .get()

// Greater than
const recentTodos = await todos
  .where('createdAt', '>', Date.now() - 86400000)
  .get()

// In array
const priorityTodos = await todos
  .where('priority', 'in', ['high', 'urgent'])
  .get()

// Array contains
const taggedTodos = await todos
  .where('tags', 'contains', 'work')
  .get()

orderBy(field, direction)

Sort results by field.

// Ascending (default)
const oldestFirst = await todos
  .orderBy('createdAt', 'asc')
  .get()

// Descending
const newestFirst = await todos
  .orderBy('createdAt', 'desc')
  .get()

// Multiple sorts
const sorted = await todos
  .orderBy('priority', 'desc')
  .orderBy('createdAt', 'asc')
  .get()

limit(n)

Limit number of results.

const first10 = await todos
  .limit(10)
  .get()

offset(n)

Skip first n results (for pagination).

const page2 = await todos
  .orderBy('createdAt', 'desc')
  .limit(20)
  .offset(20)
  .get()

Complex Queries

Chain multiple query methods:

const results = await todos
  .where('completed', '==', false)
  .where('priority', '==', 'high')
  .orderBy('createdAt', 'desc')
  .limit(10)
  .get()

Real-Time Subscriptions

onSnapshot(callback)

Subscribe to real-time updates.

// Subscribe to all documents
const unsubscribe = todos.onSnapshot((snapshot) => {
  console.log('Current todos:', snapshot)
  snapshot.forEach(doc => {
    console.log(doc.id, doc.data())
  })
})

// Unsubscribe later
unsubscribe()

Query Subscriptions

Subscribe to filtered results:

const unsubscribe = todos
  .where('completed', '==', false)
  .orderBy('createdAt', 'desc')
  .onSnapshot((snapshot) => {
    console.log('Incomplete todos:', snapshot)
  })

Document Subscriptions

Subscribe to a single document:

const unsubscribe = todos.doc('task_123').onSnapshot((doc) => {
  console.log('Task updated:', doc.data())
})

Batch Operations

Perform multiple operations atomically.

const batch = rtdb.batch()

// Create
batch.create(todos, {
  title: 'Task 1',
  completed: false
})

// Update
batch.update(todos.doc('task_123'), {
  completed: true
})

// Delete
batch.delete(todos.doc('task_456'))

// Commit all operations
await batch.commit()

Computed Values

Use special operators for server-side computation.

Increment

await todos.doc('task_123').update({
  views: { $op: 'increment', $value: 1 }
})

Timestamp

await todos.doc('task_123').update({
  updatedAt: { $fn: 'now' }
})

Query Reference

await todos.create({
  title: 'Assigned task',
  assignee: {
    $query: {
      collection: 'users',
      where: [['email', '==', 'alice@example.com']],
      limit: 1
    }
  }
})

TypeScript Support

Full type safety with generics:

interface Todo {
  id?: string
  title: string
  completed: boolean
  createdAt: number
  tags?: string[]
}

const todos = rtdb.collection<Todo>('todos')

// TypeScript knows the shape
const todo = await todos.doc('task_123').get()
console.log(todo.data().title) // ✅ Typed as string
console.log(todo.data().invalid) // ❌ TypeScript error

React Integration

import { useEffect, useState } from 'react'
import { useApi } from '@cloudillo/react'
import { RtdbClient } from '@cloudillo/rtdb'

function TodoList() {
  const api = useApi()
  const [todos, setTodos] = useState([])

  useEffect(() => {
    const rtdb = new RtdbClient({
      fileId: 'my-todos',
      token: api.token,
      url: 'wss://server.com/ws/rtdb'
    })

    const unsubscribe = rtdb.collection('todos')
      .where('completed', '==', false)
      .orderBy('createdAt', 'desc')
      .onSnapshot(setTodos)

    return () => unsubscribe()
  }, [api])

  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

Best Practices

1. Use Subscriptions Wisely

// ✅ Subscribe once per component
useEffect(() => {
  const unsubscribe = todos.onSnapshot(setData)
  return () => unsubscribe()
}, [])

// ❌ Don't create subscriptions in render
function TodoList() {
  todos.onSnapshot(setData) // Creates new subscription on every render!
}

2. Clean Up Subscriptions

// Always unsubscribe to prevent memory leaks
const unsubscribe = todos.onSnapshot(callback)

// Later or on component unmount
unsubscribe()

3. Use Batch for Multiple Operations

// ✅ Atomic batch operation
const batch = rtdb.batch()
batch.create(todos, { title: 'Task 1' })
batch.create(todos, { title: 'Task 2' })
await batch.commit()

// ❌ Multiple separate requests
await todos.create({ title: 'Task 1' })
await todos.create({ title: 'Task 2' })

4. Optimize Queries

// ✅ Specific query
const recent = await todos
  .where('createdAt', '>', yesterday)
  .limit(20)
  .get()

// ❌ Fetch all then filter in code
const all = await todos.get()
const recent = all.filter(t => t.createdAt > yesterday).slice(0, 20)

See Also