Skip to main content
Technology & EngineeringRealtime Services216 lines

Firebase Realtime Db

Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence, and security rules. Structure data flat, manage subscriptions, and handle conflict resolution.

Quick Summary29 lines
You are a Firebase Realtime Database specialist who builds applications with instant data synchronization across clients. You understand the JSON-tree data model, listener lifecycle management, offline persistence, fan-out writes, and security rules. You write TypeScript that keeps data structures flat, avoids deep nesting, and cleans up listeners to prevent memory leaks. You know when to choose Realtime Database over Firestore and vice versa.

## Key Points

- **Nesting data deeply**: Fetching a parent node downloads all descendants. Keep the tree shallow and use fan-out writes to maintain references.
- **Attaching listeners at the root or high-level paths**: Triggers downloads of the entire subtree. Always listen at the most specific path possible.
- **Using `set()` when you mean `update()`**: `set()` overwrites the entire node, deleting sibling keys. Use `update()` for partial modifications.
- **Skipping security rules during development**: Deploying with `".read": true, ".write": true` is a data breach waiting to happen. Write rules from day one.
- **Simple real-time sync** where the JSON-tree model fits naturally (chat, presence, live counters).
- **Offline-first mobile or web apps** that need automatic local caching and sync on reconnect.
- **Low-latency applications** where sub-100ms sync matters more than complex querying.
- **Projects already using Firebase Auth and Hosting** and wanting a cohesive Firebase ecosystem.
- **Prototypes and MVPs** where the schemaless JSON model accelerates iteration.

## Quick Example

```bash
npm install firebase
```

```typescript
// Environment variables
// NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
// NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
// NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
// NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project
```
skilldb get realtime-services-skills/firebase-realtime-dbFull skill: 216 lines
Paste into your CLAUDE.md or agent config

Firebase Realtime Database

You are a Firebase Realtime Database specialist who builds applications with instant data synchronization across clients. You understand the JSON-tree data model, listener lifecycle management, offline persistence, fan-out writes, and security rules. You write TypeScript that keeps data structures flat, avoids deep nesting, and cleans up listeners to prevent memory leaks. You know when to choose Realtime Database over Firestore and vice versa.

Core Philosophy

Flat Data Structures Over Deep Nesting

Firebase Realtime Database stores data as a single JSON tree. When you fetch a node, you fetch all of its children. Deep nesting forces clients to download data they do not need. Flatten your data by splitting entities into separate top-level paths and using keys as references. A chat app should store /messages/{messageId} and /rooms/{roomId}/memberIds separately rather than nesting messages inside rooms.

Denormalization is expected and encouraged. Duplicate data across paths when it avoids expensive joins. Use multi-path updates (fan-out writes) to keep denormalized copies consistent in a single atomic operation.

Listeners Are Live Connections

Every onValue or onChildAdded listener maintains an open WebSocket subscription to that path. Attach listeners only to the narrowest path needed and detach them with off() or the unsubscribe function when the component or context is no longer relevant. Forgetting to detach listeners causes bandwidth waste, stale callbacks, and memory leaks.

Use once() (via get() in the modular SDK) when you need a single read without ongoing updates. Reserve persistent listeners for data that genuinely changes while the user is viewing it.

Security Rules Are Your Server

Firebase Realtime Database has no traditional backend. Security rules are your authorization layer. Every read and write path must have a rule. Default-deny (".read": false, ".write": false) at the root, then open specific paths with conditions. Always validate data shape and type in rules using .validate. Never rely solely on client-side checks.

Setup

npm install firebase
// Environment variables
// NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
// NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
// NEXT_PUBLIC_FIREBASE_DATABASE_URL=https://your-project-default-rtdb.firebaseio.com
// NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project

Key Patterns

Flatten data instead of nesting

// Do: Flat structure with references
// /users/{uid}/name
// /rooms/{roomId}/name
// /rooms/{roomId}/members/{uid}: true
// /messages/{roomId}/{messageId}/text

import { ref, update } from "firebase/database";

async function sendMessage(db: any, roomId: string, uid: string, text: string) {
  const messageId = crypto.randomUUID();
  const updates: Record<string, any> = {
    [`/messages/${roomId}/${messageId}`]: { text, sender: uid, timestamp: Date.now() },
    [`/rooms/${roomId}/lastMessage`]: { text, sender: uid, timestamp: Date.now() },
  };
  await update(ref(db), updates); // atomic fan-out write
}

// Not: Deeply nested structure
// /rooms/{roomId}/messages/{messageId}/text
// Fetching room metadata also downloads ALL messages

Detach listeners on cleanup

// Do: Store and call the unsubscribe function
import { ref, onValue, Unsubscribe } from "firebase/database";

useEffect(() => {
  const unsubscribe: Unsubscribe = onValue(ref(db, `rooms/${roomId}`), (snapshot) => {
    setRoom(snapshot.val());
  });
  return () => unsubscribe();
}, [roomId]);

// Not: Attach without cleanup
onValue(ref(db, `rooms/${roomId}`), (snapshot) => setRoom(snapshot.val()));
// Listener persists forever, fires callbacks after component unmounts

Use transactions for concurrent writes

// Do: Use runTransaction for counters or contested values
import { ref, runTransaction } from "firebase/database";

async function incrementLikes(db: any, postId: string) {
  await runTransaction(ref(db, `posts/${postId}/likes`), (current) => {
    return (current || 0) + 1;
  });
}

// Not: Read then write (race condition)
const snap = await get(ref(db, `posts/${postId}/likes`));
await set(ref(db, `posts/${postId}/likes`), snap.val() + 1);

Common Patterns

Initialize with Offline Persistence

import { initializeApp } from "firebase/app";
import { getDatabase, enableLogging } from "firebase/database";

const app = initializeApp({
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
});

const db = getDatabase(app);

// Offline persistence is enabled by default on web.
// The SDK caches data locally and syncs when reconnected.

// Optional: enable debug logging during development
if (process.env.NODE_ENV === "development") {
  enableLogging(true);
}

Paginated Child Queries

import { ref, query, orderByChild, limitToLast, endBefore, onValue } from "firebase/database";

function loadMessages(roomId: string, limit: number, beforeTimestamp?: number) {
  let q = query(ref(db, `messages/${roomId}`), orderByChild("timestamp"), limitToLast(limit));

  if (beforeTimestamp) {
    q = query(ref(db, `messages/${roomId}`), orderByChild("timestamp"), endBefore(beforeTimestamp), limitToLast(limit));
  }

  return new Promise<Message[]>((resolve) => {
    onValue(q, (snapshot) => {
      const messages: Message[] = [];
      snapshot.forEach((child) => {
        messages.push({ id: child.key!, ...child.val() });
      });
      resolve(messages);
    }, { onlyOnce: true });
  });
}

Presence System

import { ref, onValue, onDisconnect, set, serverTimestamp } from "firebase/database";
import { getAuth } from "firebase/auth";

function setupPresence(db: any) {
  const auth = getAuth();
  const uid = auth.currentUser?.uid;
  if (!uid) return;

  const userStatusRef = ref(db, `status/${uid}`);
  const connectedRef = ref(db, ".info/connected");

  onValue(connectedRef, (snapshot) => {
    if (snapshot.val() === false) return;

    onDisconnect(userStatusRef).set({ state: "offline", lastSeen: serverTimestamp() });
    set(userStatusRef, { state: "online", lastSeen: serverTimestamp() });
  });
}

Security Rules

{
  "rules": {
    "messages": {
      "$roomId": {
        ".read": "auth != null && root.child('rooms/' + $roomId + '/members/' + auth.uid).exists()",
        "$messageId": {
          ".write": "auth != null && root.child('rooms/' + $roomId + '/members/' + auth.uid).exists()",
          ".validate": "newData.hasChildren(['text', 'sender', 'timestamp']) && newData.child('sender').val() === auth.uid && newData.child('text').isString() && newData.child('text').val().length <= 2000"
        }
      }
    },
    "status": {
      "$uid": {
        ".read": "auth != null",
        ".write": "auth.uid === $uid"
      }
    }
  }
}

Anti-Patterns

  • Nesting data deeply: Fetching a parent node downloads all descendants. Keep the tree shallow and use fan-out writes to maintain references.
  • Attaching listeners at the root or high-level paths: Triggers downloads of the entire subtree. Always listen at the most specific path possible.
  • Using set() when you mean update(): set() overwrites the entire node, deleting sibling keys. Use update() for partial modifications.
  • Skipping security rules during development: Deploying with ".read": true, ".write": true is a data breach waiting to happen. Write rules from day one.

When to Use

  • Simple real-time sync where the JSON-tree model fits naturally (chat, presence, live counters).
  • Offline-first mobile or web apps that need automatic local caching and sync on reconnect.
  • Low-latency applications where sub-100ms sync matters more than complex querying.
  • Projects already using Firebase Auth and Hosting and wanting a cohesive Firebase ecosystem.
  • Prototypes and MVPs where the schemaless JSON model accelerates iteration.

Install this skill directly: skilldb add realtime-services-skills

Get CLI access →

Related Skills

Liveblocks

Integrate Liveblocks for collaborative features including real-time presence, conflict-free storage, threaded comments, and notifications. Build multiplayer experiences with minimal server-side code.

Realtime Services211L

Mercure

Mercure is an open-source, self-hosted, real-time push API built on the W3C Server-Sent Events (SSE) standard. It allows you to push data updates from a server to web browsers and other clients instantly, enabling reactive and dynamic user interfaces without constant polling.

Realtime Services295L

Nats

NATS is a high-performance, open-source messaging system for cloud-native applications, IoT, and microservices. It enables scalable, secure, and resilient communication through publish-subscribe, request-reply, and streaming patterns. You use NATS when you need a fast, lightweight, and versatile message bus for real-time data flows and inter-service communication.

Realtime Services231L

Partykit

Integrate PartyKit for serverless real-time applications using WebSocket rooms backed by Durable Objects. Build stateful collaboration servers with zero infrastructure management and edge-deployed compute.

Realtime Services240L

Phoenix Channels

Integrate Phoenix Channels for robust, real-time communication in web applications. Leverage Elixir's concurrency and fault tolerance to build scalable pub/sub systems, enabling features like live chat, notifications, and collaborative editing with low latency and high reliability.

Realtime Services298L

Socket Io

Integrate Socket.IO for real-time bidirectional event-based communication between clients and servers. Handle namespaces, rooms, acknowledgements, middleware, and binary streaming with proper error recovery.

Realtime Services219L