1. Pengenalan Firebase & Firestore
Cloud Firestore adalah database NoSQL berbasis cloud dari Google, bagian dari ekosistem Firebase. Firestore menyimpan data dalam bentuk dokumen (document) dan koleksi (collection) — bukan tabel dan baris seperti database relasional.
Keunggulan utama Firestore: real-time sync (data berubah otomatis di semua client), offline support (bekerja tanpa internet), scalability otomatis, dan integrasi langsung dengan Firebase Auth, Cloud Functions, dan layanan Google Cloud lainnya.
┌─────────────────────────────────────────────────────────────────┐ │ CLOUD FIRESTORE │ │ │ │ Database │ │ ├── Collection: "users" │ │ │ ├── Document: "user_001" │ │ │ │ ├── name: "Budi Santoso" │ │ │ │ ├── email: "budi@email.com" │ │ │ │ ├── age: 28 │ │ │ │ ├── city: "Jakarta" │ │ │ │ └── Sub-collection: "posts" │ │ │ │ ├── Document: "post_001" │ │ │ │ │ ├── title: "Hello World" │ │ │ │ │ ├── content: "Postingan pertama..." │ │ │ │ │ └── createdAt: Timestamp │ │ │ │ └── Document: "post_002" │ │ │ │ ├── title: "Belajar Firestore" │ │ │ │ └── ... │ │ │ └── Document: "user_002" │ │ │ ├── name: "Sari Dewi" │ │ │ └── ... │ │ ├── Collection: "products" │ │ │ ├── Document: "prod_001" │ │ │ │ ├── name: "Laptop Pro" │ │ │ │ ├── price: 15000000 │ │ │ │ └── tags: ["elektronik", "laptop"] │ │ │ └── ... │ │ └── Collection: "orders" │ │ └── ... │ │ │ │ Hierarchy: Database → Collection → Document → Sub-collection │ │ No SQL, no tables — fleksibel dan schema-free! │ └─────────────────────────────────────────────────────────────────┘
Firestore vs Realtime Database
| Fitur | Cloud Firestore | Realtime Database |
|---|---|---|
| Data Model | Collection-Document | JSON tree besar |
| Query | Kaya — filter, sort, combine | Dasar — single filter |
| Real-time | ✅ Snapshot listeners | ✅ Value listeners |
| Offline | ✅ Web & Mobile | ✅ Mobile only |
| Scalability | Otomatis, lebih baik | Perlu sharding manual |
| Harga | Per operation + bandwidth | Per bandwidth |
| Sub-collection | ✅ Didukung | ❌ Data nested |
2. Setup & Inisialisasi
// =============================================
// STEP 1: Install Firebase SDK
// =============================================
// npm install firebase
// atau gunakan CDN
// =============================================
// STEP 2: Inisialisasi Firebase
// =============================================
import { initializeApp } from 'firebase/app';
import {
getFirestore,
collection,
doc,
getDoc,
getDocs,
addDoc,
setDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
onSnapshot,
serverTimestamp,
Timestamp,
writeBatch,
runTransaction
} from 'firebase/firestore';
// Konfigurasi dari Firebase Console
const firebaseConfig = {
apiKey: "AIzaSy...",
authDomain: "my-app.firebaseapp.com",
projectId: "my-app-id",
storageBucket: "my-app.appspot.com",
messagingSenderId: "123456789",
appId: "1:123456789:web:abc123"
};
// Inisialisasi
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// Sekarang 'db' siap digunakan untuk semua operasi Firestore
// =============================================
// STEP 3: Firebase Console
// =============================================
// 1. Buka https://console.firebase.google.com
// 2. Buat project baru atau pilih existing
// 3. Aktifkan Cloud Firestore (Build → Firestore Database)
// 4. Pilih mode: Production Mode (secure by default) atau Test Mode
// 5. Pilih lokasi server (asia-southeast1 untuk Indonesia)
3. CRUD Operations — Create, Read, Update, Delete
// =============================================
// CREATE: Menambahkan dokumen baru
// =============================================
// Method 1: addDoc — auto-generate ID
const usersRef = collection(db, 'users');
const newUser = await addDoc(usersRef, {
name: 'Budi Santoso',
email: 'budi@email.com',
age: 28,
city: 'Jakarta',
hobbies: ['coding', 'gaming', 'reading'],
createdAt: serverTimestamp(),
isActive: true
});
console.log('User created with ID:', newUser.id);
// Output: User created with ID: a1b2c3d4e5
// Method 2: setDoc — tentukan ID sendiri
const userRef = doc(db, 'users', 'user_budi');
await setDoc(userRef, {
name: 'Budi Santoso',
email: 'budi@email.com',
age: 28,
city: 'Jakarta',
createdAt: serverTimestamp()
});
// Document ID = 'user_budi'
// Method 3: setDoc dengan merge (update jika sudah ada)
await setDoc(userRef, {
age: 29,
updatedAt: serverTimestamp()
}, { merge: true });
// Hanya field 'age' dan 'updatedAt' yang berubah
// =============================================
// READ: Membaca data
// =============================================
// Baca satu dokumen
const userSnap = await getDoc(userRef);
if (userSnap.exists()) {
const data = userSnap.data();
console.log(`${data.name}, ${data.age} tahun, ${data.city}`);
} else {
console.log('Dokumen tidak ditemukan');
}
// Baca semua dokumen dalam collection
const allUsers = await getDocs(collection(db, 'users'));
allUsers.forEach(doc => {
console.log(`${doc.id}: ${doc.data().name}`);
});
// Baca dengan ID dari snapshot
const userId = userSnap.id;
const userData = userSnap.data();
// =============================================
// UPDATE: Memperbarui dokumen
// =============================================
// updateDoc — hanya update field yang ditentukan
await updateDoc(userRef, {
age: 30,
city: 'Bandung',
updatedAt: serverTimestamp()
});
// Field lain tetap tidak berubah
// =============================================
// DELETE: Menghapus dokumen
// =============================================
// Hapus satu dokumen
await deleteDoc(userRef);
// ⚠️ Firestore TIDAK punya "delete collection" langsung
// Harus hapus satu per satu atau gunakan Cloud Functions / Admin SDK
// Hapus field dari dokumen (bukan hapus dokumen)
const { deleteField } = await import('firebase/firestore');
await updateDoc(userRef, {
email: deleteField()
});
// =============================================
// SUB-COLLECTIONS: Data bersarang
// =============================================
// Tambah post ke sub-collection 'posts' milik user
const postsRef = collection(db, 'users', 'user_budi', 'posts');
await addDoc(postsRef, {
title: 'Belajar Firestore',
content: 'Firestore itu mudah dan powerful!',
tags: ['tutorial', 'firebase'],
createdAt: serverTimestamp()
});
// Baca semua posts milik user
const userPosts = await getDocs(postsRef);
userPosts.forEach(post => {
console.log(`${post.id}: ${post.data().title}`);
});
Tipe Data yang Didukung Firestore
| Tipe Data | JavaScript | Contoh |
|---|---|---|
| String | string | "Budi" |
| Number | number | 28, 15.5 |
| Boolean | boolean | true, false |
| Timestamp | Timestamp | serverTimestamp() |
| GeoPoint | GeoPoint | new GeoPoint(-6.2, 106.8) |
| Array | array | ["a", "b", "c"] |
| Map (Object) | object | {key: "value"} |
| Reference | DocumentReference | doc(db, "users", "id") |
| null | null | null |
4. Queries — Filter, Sort & Limit
// =============================================
// BASIC QUERY: where filter
// =============================================
// Cari user dari Jakarta
const jakartaQuery = query(
collection(db, 'users'),
where('city', '==', 'Jakarta')
);
const snapshot = await getDocs(jakartaQuery);
snapshot.forEach(doc => console.log(doc.data().name));
// Operator perbandingan yang tersedia:
// == (sama dengan)
// != (tidak sama)
// < (kurang dari)
// <= (kurang dari atau sama)
// > (lebih dari)
// >= (lebih dari atau sama)
// in (dalam array nilai)
// not-in (tidak dalam array)
// array-contains (array mengandung nilai)
// array-contains-any (array mengandung salah satu)
// =============================================
// COMPOUND QUERY: Kombinasi filter
// =============================================
// User dari Jakarta yang umur > 25
const compoundQuery = query(
collection(db, 'users'),
where('city', '==', 'Jakarta'),
where('age', '>', 25),
orderBy('age', 'desc'),
limit(10)
);
const results = await getDocs(compoundQuery);
// =============================================
// OPERATOR IN: Filter dengan banyak nilai
// =============================================
const multiCityQuery = query(
collection(db, 'users'),
where('city', 'in', ['Jakarta', 'Bandung', 'Surabaya'])
);
// Maksimal 10 nilai dalam 'in'
const notInQuery = query(
collection(db, 'users'),
where('city', 'not-in', ['Jakarta', 'Bandung'])
);
// =============================================
// ARRAY OPERATIONS
// =============================================
// Array contains satu nilai
const hobbyQuery = query(
collection(db, 'users'),
where('hobbies', 'array-contains', 'coding')
);
// Cocok untuk tags, categories, hobbies
// Array contains salah satu dari beberapa nilai
const multiHobbyQuery = query(
collection(db, 'users'),
where('hobbies', 'array-contains-any', ['coding', 'gaming'])
);
// Maksimal 10 nilai
// =============================================
// ORDERING & LIMITING
// =============================================
// Sort ascending (default)
const sortedAsc = query(
collection(db, 'users'),
orderBy('name', 'asc')
);
// Sort descending
const sortedDesc = query(
collection(db, 'users'),
orderBy('createdAt', 'desc'),
limit(20) // Ambil 20 terbaru
);
// Multiple sort
const multiSort = query(
collection(db, 'users'),
orderBy('city', 'asc'),
orderBy('age', 'desc')
);
// Pagination: startAfter
const page1 = query(
collection(db, 'users'),
orderBy('name'),
limit(10)
);
const page1Snap = await getDocs(page1);
const lastDoc = page1Snap.docs[page1Snap.docs.length - 1];
// Halaman berikutnya
const page2 = query(
collection(db, 'users'),
orderBy('name'),
startAfter(lastDoc),
limit(10)
);
// =============================================
// AGGREGATION QUERIES (COUNT, SUM, AVG)
// =============================================
// Count documents (lebih murah dari membaca semua data)
import { count, sum, average } from 'firebase/firestore';
const countQuery = query(
collection(db, 'users'),
where('city', '==', 'Jakarta')
);
const countSnap = await getCountFromServer(countQuery);
console.log('Total user Jakarta:', countSnap.data().count);
// Sum dan Average
const sumSnap = await getAggregateFromServer(
collection(db, 'orders'),
{ totalPrice: sum('total'), avgPrice: average('total') }
);
console.log('Total:', sumSnap.data().totalPrice);
console.log('Rata-rata:', sumSnap.data().avgPrice);
// =============================================
// BATASAN QUERY FIRESTORE
// =============================================
// ⚠️ Tidak bisa: OR query (harus gabung manual)
// ⚠️ Tidak bisa: != pada 2 field berbeda
// ⚠️ Tidak bisa: range filter pada 2 field berbeda
// ⚠️ Tidak bisa: full-text search (butuh Algolia / Typesense)
// ⚠️ Tidak bisa: join antar collection (desain data yang baik)
Firestore punya aturan ketat: jika Anda menggunakan inequality filter (<, >, <=, >=, !=) dan orderBy, field yang di-orderBy harus sama dengan field inequality pertama. Ini disebut inequality + orderBy constraint. Rencanakan index Anda dengan hati-hati.
5. Real-time Listeners & Data Sync
Fitur paling powerful dari Firestore: data bisa berubah secara real-time di semua client yang terhubung. Saat satu user mengubah data, semua user lain langsung melihat perubahan tanpa refresh.
// =============================================
// REAL-TIME LISTENER: Satu dokumen
// =============================================
import { onSnapshot } from 'firebase/firestore';
const userRef = doc(db, 'users', 'user_budi');
// Listener aktif — otomatis update saat data berubah
const unsubscribe = onSnapshot(userRef, (snapshot) => {
if (snapshot.exists()) {
const data = snapshot.data();
console.log('Data terbaru:', data.name, data.age);
// Update UI di sini
document.getElementById('userName').textContent = data.name;
document.getElementById('userAge').textContent = data.age;
} else {
console.log('Dokumen sudah dihapus');
}
}, (error) => {
console.error('Error listening:', error);
});
// Hentikan listener saat tidak diperlukan
// unsubscribe();
// =============================================
// REAL-TIME LISTENER: Collection / Query
// =============================================
const chatRef = query(
collection(db, 'messages'),
where('room', '==', 'general'),
orderBy('createdAt', 'desc'),
limit(50)
);
const unsubChat = onSnapshot(chatRef, (snapshot) => {
snapshot.docChanges().forEach((change) => {
if (change.type === 'added') {
console.log('Pesan baru:', change.doc.data().text);
// Tambahkan ke UI
addMessageToUI(change.doc.data());
}
if (change.type === 'modified') {
console.log('Pesan diubah:', change.doc.data().text);
// Update di UI
updateMessageInUI(change.doc.id, change.doc.data());
}
if (change.type === 'removed') {
console.log('Pesan dihapus:', change.doc.id);
// Hapus dari UI
removeMessageFromUI(change.doc.id);
}
});
});
// =============================================
// REAL-TIME: Data sync untuk dashboard
// =============================================
function setupDashboard() {
// Listener untuk orders
const ordersQuery = query(
collection(db, 'orders'),
where('status', '==', 'pending'),
orderBy('createdAt', 'desc')
);
const unsub = onSnapshot(ordersQuery, (snapshot) => {
const orders = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}));
// Update dashboard
updatePendingOrders(orders.length);
updateOrdersList(orders);
updateTotalValue(orders.reduce((sum, o) => sum + o.total, 0));
});
return unsub; // Simpan untuk cleanup
}
// Cleanup saat component unmount (React)
// useEffect(() => {
// const unsub = setupDashboard();
// return () => unsub(); // Hentikan listener
// }, []);
// =============================================
// REAL-TIME: Online/Offline Status
// =============================================
import { enableIndexedDbPersistence } from 'firebase/firestore';
// Aktifkan offline persistence (default di mobile SDK)
try {
await enableIndexedDbPersistence(db);
console.log('Offline persistence aktif');
} catch (err) {
if (err.code === 'failed-precondition') {
console.warn('Multiple tabs open, persistence hanya 1 tab');
} else if (err.code === 'unimplemented') {
console.warn('Browser tidak support persistence');
}
}
6. Security Rules — Mengamankan Data
Firestore Security Rules adalah lapisan keamanan yang mengontrol siapa yang bisa membaca, menulis, dan menghapus data. Ini adalah satu-satunya firewall antara client dan data Anda — sangat penting!
// =============================================
// FILE: firestore.rules
// =============================================
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ========================================
// RULE: Default — blokir semua akses
// ========================================
// match /{document=**} {
// allow read, write: if false;
// }
// ========================================
// RULE: Users collection
// ========================================
match /users/{userId} {
// Siapapun bisa baca profil user
allow read: if true;
// Hanya user sendiri yang bisa buat/update profilnya
allow create: if request.auth != null
&& request.auth.uid == userId
&& isValidUserData(request.resource.data);
allow update: if request.auth != null
&& request.auth.uid == userId
&& isValidUserData(request.resource.data);
// Hanya admin atau user sendiri yang bisa hapus
allow delete: if request.auth != null
&& (request.auth.uid == userId
|| isAdmin(request.auth.uid));
// Sub-collection: posts
match /posts/{postId} {
allow read: if true;
allow create: if request.auth != null
&& request.auth.uid == userId;
allow update: if request.auth != null
&& (request.auth.uid == userId
|| isModerator(request.auth.uid));
allow delete: if request.auth != null
&& (request.auth.uid == userId
|| isAdmin(request.auth.uid));
}
}
// ========================================
// RULE: Messages (chat)
// ========================================
match /messages/{messageId} {
allow read: if request.auth != null;
allow create: if request.auth != null
&& request.resource.data.text is string
&& request.resource.data.text.size() <= 500
&& request.resource.data.senderId == request.auth.uid;
// Tidak boleh edit pesan orang lain
allow update: if request.auth != null
&& resource.data.senderId == request.auth.uid;
allow delete: if isAdmin(request.auth.uid);
}
// ========================================
// RULE: Orders
// ========================================
match /orders/{orderId} {
allow read: if request.auth != null
&& (resource.data.userId == request.auth.uid
|| isAdmin(request.auth.uid));
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.total > 0;
allow update: if isAdmin(request.auth.uid);
allow delete: if false; // Orders tidak boleh dihapus
}
// ========================================
// HELPER FUNCTIONS
// ========================================
// Cek apakah user adalah admin
function isAdmin(uid) {
return get(/databases/$(database)/documents/users/$(uid)).data
.role == 'admin';
}
// Cek moderator
function isModerator(uid) {
return get(/databases/$(database)/documents/users/$(uid)).data
.role in ['admin', 'moderator'];
}
// Validasi data user
function isValidUserData(data) {
return data.name is string
&& data.name.size() >= 2
&& data.name.size() <= 100
&& data.email is string;
}
}
}
- Selalu mulai dengan "deny all" — buka akses hanya yang diperlukan
- Validasi data di server-side — jangan percaya client
- Test rules di Firebase Console → Rules Simulator
- Gunakan helper functions untuk reusable logic
- Perhatikan performance —
get()di rules = 1 read operation
7. Composite Indexes & Query Limitations
// =============================================
// SINGLE-FIELD INDEX: Otomatis dibuat
// =============================================
// Firestore otomatis membuat index untuk setiap field
// Tidak perlu setup manual untuk single-field query
// Query ini pakai auto index:
const q = query(
collection(db, 'users'),
where('city', '==', 'Jakarta')
);
// =============================================
// COMPOSITE INDEX: Butuh setup manual
// =============================================
// Diperlukan ketika query menggabungkan beberapa field
// dengan filter dan orderBy berbeda
// Query ini butuh composite index:
const q2 = query(
collection(db, 'users'),
where('city', '==', 'Jakarta'),
where('age', '>', 25),
orderBy('age', 'desc')
);
// → Butuh composite index: city ASC, age DESC
// Saat pertama kali menjalankan query yang butuh composite index,
// Firestore akan memberikan error dengan LINK untuk membuat index.
// Klik link tersebut → index otomatis dibuat!
// =============================================
// Mengelola Indexes
// =============================================
// Firebase Console → Firestore → Indexes
// Manual: file firestore.indexes.json
{
"indexes": [
{
"collectionGroup": "users",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "city", "order": "ASCENDING" },
{ "fieldPath": "age", "order": "DESCENDING" }
]
},
{
"collectionGroup": "orders",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "userId", "order": "ASCENDING" },
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
]
}
// =============================================
// BATCH OPERATIONS: Banyak operasi sekaligus
// =============================================
import { writeBatch } from 'firebase/firestore';
const batch = writeBatch(db);
// Set beberapa dokumen sekaligus
batch.set(doc(db, 'users', 'user1'), { name: 'User 1' });
batch.set(doc(db, 'users', 'user2'), { name: 'User 2' });
batch.update(doc(db, 'users', 'user3'), { age: 30 });
batch.delete(doc(db, 'users', 'user_old'));
await batch.commit();
// Maksimal 500 operasi per batch
// =============================================
// TRANSACTIONS: Atomic operations
// =============================================
import { runTransaction } from 'firebase/firestore';
async function transferBalance(fromId, toId, amount) {
await runTransaction(db, async (transaction) => {
const fromRef = doc(db, 'users', fromId);
const toRef = doc(db, 'users', toId);
const fromDoc = await transaction.get(fromRef);
const toDoc = await transaction.get(toRef);
if (!fromDoc.exists() || !toDoc.exists()) {
throw new Error('User tidak ditemukan');
}
const fromBalance = fromDoc.data().balance;
if (fromBalance < amount) {
throw new Error('Saldo tidak cukup');
}
// Atomic: kedua update berhasil atau keduanya gagal
transaction.update(fromRef, {
balance: fromBalance - amount
});
transaction.update(toRef, {
balance: toDoc.data().balance + amount
});
});
}
8. Best Practices & Data Modeling
Denormalisasi: Data Duplikat Itu Oke
Di Firestore, denormalisasi (menyimpan data duplikat) adalah hal normal dan direkomendasikan. Ini karena Firestore tidak punya JOIN — membaca 1 dokumen jauh lebih murah dari membaca 2 dokumen.
// =============================================
// DATA MODELING: Denormalisasi
// =============================================
// ❌ BURUK: Referencing (seperti foreign key)
// {
// userId: "user_001", → harus fetch lagi untuk dapat nama
// message: "Hello"
// }
// ✅ BAIK: Denormalisasi (simpan data yang sering dibaca)
// {
// userId: "user_001",
// userName: "Budi Santoso", ← tidak perlu fetch lagi
// userAvatar: "https://...",
// message: "Hello"
// }
// =============================================
// KAPAN SUB-COLLECTION vs COLLECTION TERPISAH?
// =============================================
// Sub-collection: data selalu diakses dari parent
// Contoh: posts milik user
// users/user_001/posts/post_001
// Collection terpisah: data diakses secara independen
// Contoh: orders bisa diakses dari banyak tempat
// orders/order_001
// =============================================
// COST OPTIMIZATION: hemat pembacaan
// =============================================
// ❌ BURUK: Baca semua field padahal butuh 2
const doc1 = await getDoc(userRef);
// Membaca 20 fields = 1 read, tapi bandwidth besar
// ✅ BAIK: Pilih field yang dibutuhkan
import { select } from 'firebase/firestore';
const doc2 = await getDoc(userRef, { select: ['name', 'email'] });
// Hanya transfer 2 fields
// =============================================
// SECURITY: Jangan simpan data sensitif tanpa enkripsi
// =============================================
// ❌ BURUK: Simpan password di Firestore
// { password: "rahasia123" }
// ✅ BAIK: Gunakan Firebase Auth untuk authentication
// Firestore hanya simpan data non-sensitive
Pricing Model Firestore
| Operasi | Free Tier | Harga (Blaze) |
|---|---|---|
| Read (baca dokumen) | 50K/hari | $0.06 per 100K |
| Write (tulis dokumen) | 20K/hari | $0.18 per 100K |
| Delete (hapus dokumen) | 20K/hari | $0.02 per 100K |
| Storage | 1 GiB | $0.18/GiB/bulan |
| Network | 10 GiB/bulan | $0.12/GiB |
9. Quiz Pemahaman
1. Struktur data dasar Firestore adalah...
2. Fitur utama yang membedakan Firestore dari database konvensional adalah...
3. Mengapa denormalisasi (data duplikat) direkomendasikan di Firestore?
4. Apa fungsi Firestore Security Rules?
5. Kapan composite index diperlukan di Firestore?
Rangkuman
- Collection-Document model — bukan tabel-baris, fleksibel dan schema-free
- Real-time listeners — data otomatis sinkron di semua client
- Security Rules — satu-satunya firewall, jangan abaikan!
- Denormalisasi — data duplikat OKE untuk performa
- Composite index — perlu untuk multi-field queries
- Batch & Transactions — operasi atomik untuk konsistensi data
- Perhatikan pricing — setiap read/write ada biayanya