Integrating Firebase Realtime Database into Mobile Application
Firebase Realtime Database (RTDB) — JSON tree with WebSocket sync. Not relational, not document database in classic sense. Key feature — offline persistence and real-time sync out of box. But wrong data structure in RTDB turns these advantages into problem: subscribing to node with deep nesting will pull entire subtree into device memory.
Data Structure: Denormalization Mandatory
RTDB has no JOIN. If you want user posts — don't nest posts inside user. Denormalize:
{
"users": {
"uid123": { "name": "Ivan", "email": "ivan@..." }
},
"posts": {
"postId1": { "userId": "uid123", "text": "...", "createdAt": 1700000000 }
},
"userPosts": {
"uid123": { "postId1": true, "postId2": true }
}
}
userPosts — reverse index to get specific user's posts without scanning entire posts node. Standard RTDB pattern.
Subscriptions in React Native
import database from '@react-native-firebase/database';
useEffect(() => {
const ref = database().ref(`/userPosts/${userId}`);
// value: full snapshot on each change
const onValue = ref.on('value', snapshot => {
const postIds = Object.keys(snapshot.val() ?? {});
setPostIds(postIds);
});
// child_added: only new elements
const onChildAdded = ref.on('child_added', snapshot => {
setPostIds(prev => [...prev, snapshot.key!]);
});
return () => {
ref.off('value', onValue);
ref.off('child_added', onChildAdded);
};
}, [userId]);
Critical: always call ref.off() on unmount. on() without off() — memory leak: listener lives forever, rerenders unmounted component. In production this crashes with Can't perform a React state update on an unmounted component.
Offline Persistence
// index.js, before any database() calls
import database from '@react-native-firebase/database';
database().setPersistenceEnabled(true);
database().setPersistenceCacheSizeBytes(10 * 1024 * 1024); // 10 MB
setPersistenceEnabled(true) enables SQLite cache on device. Offline, app reads from cache. On network recovery — syncs changes. Call only once at init, before first database access.
keepSynced(true) on specific node — pre-loads data and keeps in cache even without active listeners:
database().ref(`/userPosts/${userId}`).keepSynced(true);
Careful: don't apply keepSynced to large nodes — RTDB downloads entire tree.
Security Rules
Default RTDB rules — either everyone read/write or nobody. Must configure before production:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"posts": {
"$postId": {
".read": "auth != null",
".write": "auth != null && newData.child('userId').val() === auth.uid",
".validate": "newData.hasChildren(['userId', 'text', 'createdAt'])"
}
}
}
}
.validate — checks data structure before write. Without validation, client can write arbitrary JSON.
Transactions for Concurrent Updates
Likes, counters, balance — any concurrent increment:
const likeRef = database().ref(`/posts/${postId}/likes`);
await likeRef.transaction(currentLikes => (currentLikes ?? 0) + 1);
transaction() atomically reads and writes. If another client changed between read and write — transaction repeats automatically (up to 25 times). For likes this is the only correct approach — set(currentLikes + 1) gives race condition on simultaneous taps.
RTDB vs Firestore: When to Choose What
RTDB better for: real-time chats, presence/online statuses, game leaderboards, event streams. Firestore better for: complex queries, document collections, scaling > 1M users.
RTDB cost: $5/GB storage + $1/GB traffic. High update frequency (chat, real-time) makes RTDB cheaper than Firestore due to no cross-billing by read/write operations.
Assessment
RTDB with offline persistence, security rules and real-time subscriptions: 2–3 weeks.







