Introduction
The Firebase Realtime Database is a cloud-hosted NoSQL database where data is stored as JSON and synchronized in real-time to every connected client. In AngularFire v21, the Realtime Database provides dedicated reactive primitives like listVal() and objectVal() that bridge easily to Angular Signals.
Key Capabilities:
- Millisecond latency real-time updates over persistent WebSockets
listVal()andobjectVal()reactive streams with optional key mapping- Presence detection (
.info/connected) for tracking online/offline status- Atomic transactions for incrementing counters and financial balances
Setup & Configuration
Add provideDatabase() to your ApplicationConfig:
import { ApplicationConfig } from '@angular/core';
import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { getDatabase, provideDatabase } from '@angular/fire/database';
export const appConfig: ApplicationConfig = {
providers: [
provideFirebaseApp(() => initializeApp({ /* firebaseConfig */ })),
provideDatabase(() => getDatabase()),
],
};
Synchronizing Objects with Signals
Use objectVal() to subscribe to a specific path in your JSON tree and expose it as a reactive signal:
import { Component, inject, input } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Database, ref, objectVal } from '@angular/fire/database';
interface RoomSettings {
name: string;
maxUsers: number;
isOpen: boolean;
}
@Component({
selector: 'app-room-header',
template: `
@if (settings(); as s) {
<div class="room-header">
<h2>{{ s.name }}</h2>
<span [class.open]="s.isOpen">
{{ s.isOpen ? 'Open Room' : 'Locked' }}
</span>
</div>
}
`,
})
export class RoomHeaderComponent {
private db = inject(Database);
readonly roomId = input.required<string>();
// Real-time object signal
readonly settings = toSignal(
objectVal<RoomSettings>(ref(this.db, `rooms/${this.roomId()}/settings`))
);
}
Synchronizing Lists with Signals
Use listVal() to stream an array of children under a database reference with automatic key preservation:
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Database, ref, listVal, query, limitToLast, orderByChild } from '@angular/fire/database';
interface ChatMessage {
key: string;
sender: string;
text: string;
timestamp: number;
}
@Component({
selector: 'app-chat-feed',
template: `
<div class="chat-container">
<div class="messages">
@for (msg of messages(); track msg.key) {
<div class="message">
<strong>{{ msg.sender }}:</strong>
<span>{{ msg.text }}</span>
</div>
}
</div>
</div>
`,
})
export class ChatFeedComponent {
private db = inject(Database);
// Query: last 50 messages ordered by timestamp
private chatQuery = query(
ref(this.db, 'messages'),
orderByChild('timestamp'),
limitToLast(50)
);
// Real-time list signal with { keyField: 'key' }
readonly messages = toSignal(
listVal<ChatMessage>(this.chatQuery, { keyField: 'key' }),
{ initialValue: [] }
);
}
Writing Data & Transactions
Mutate the JSON tree using set(), update(), push(), and runTransaction():
import { Injectable, inject } from '@angular/core';
import { Database, ref, push, set, update, remove, runTransaction } from '@angular/fire/database';
@Injectable({ providedIn: 'root' })
export class ChatService {
private db = inject(Database);
// Append new item to list
async sendMessage(sender: string, text: string): Promise<void> {
const messagesRef = ref(this.db, 'messages');
const newMsgRef = push(messagesRef);
await set(newMsgRef, {
sender,
text,
timestamp: Date.now(),
});
}
// Atomic counter increment
async upvotePost(postId: string): Promise<void> {
const postVotesRef = ref(this.db, `posts/${postId}/votes`);
await runTransaction(postVotesRef, (currentVotes) => {
return (currentVotes || 0) + 1;
});
}
}
Presence Tracking (.info/connected)
Detect whether a client is currently connected or offline:
import { Injectable, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { Database, ref, objectVal, onDisconnect, set } from '@angular/fire/database';
@Injectable({ providedIn: 'root' })
export class PresenceService {
private db = inject(Database);
// Boolean signal indicating active connection
readonly isConnected = toSignal(
objectVal<boolean>(ref(this.db, '.info/connected')),
{ initialValue: false }
);
setupUserPresence(userId: string) {
const userStatusRef = ref(this.db, `status/${userId}`);
// Set offline payload on disconnect
onDisconnect(userStatusRef).set({ state: 'offline', lastChanged: Date.now() });
// Set online
set(userStatusRef, { state: 'online', lastChanged: Date.now() });
}
}