Skip to main content
Technology & EngineeringRealtime Services217 lines

Firebase Realtime Db

Integrate Firebase Realtime Database for synchronized data with listeners, offline persistence,

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: 217 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 →