Tutorial Firebase untuk Developer Indonesia: Panduan Lengkap
ID | EN

Tutorial Firebase untuk Developer Indonesia: Panduan Lengkap

Sabtu, 27 Des 2025

Firebase adalah Backend-as-a-Service (BaaS) dari Google yang udah jadi standar industri untuk rapid development. Kalau kamu mau bikin app tanpa ribet setup backend dari nol, Firebase adalah pilihan yang solid.

Di artikel ini, gue bakal bahas lengkap semua yang perlu kamu tahu tentang Firebase—dari setup sampai production. Let’s dive in!

Apa Itu Firebase?

Firebase adalah platform development dari Google yang menyediakan berbagai services:

  • Authentication - Login dengan email, Google, phone, dan provider lainnya
  • Cloud Firestore - NoSQL database dengan realtime sync
  • Realtime Database - Legacy database, masih berguna untuk use case tertentu
  • Cloud Storage - Object storage untuk files dan media
  • Cloud Functions - Serverless backend logic
  • Hosting - CDN hosting untuk web apps
  • Analytics - User behavior tracking
  • Crashlytics - Error monitoring
  • Remote Config - Feature flags dan A/B testing

Yang bikin Firebase powerful adalah integrasi seamless antar services dan SDK yang mature untuk berbagai platform (Web, iOS, Android, Flutter).

Setup Project Firebase

1. Buat Project di Firebase Console

  1. Pergi ke console.firebase.google.com
  2. Klik “Add Project”
  3. Masukkan nama project
  4. Pilih apakah mau enable Google Analytics (recommended)
  5. Tunggu beberapa detik sampai project ready

2. Tambahkan App ke Project

Klik icon web (</>) di project overview untuk register web app:

  1. Masukkan nama app
  2. Centang “Also set up Firebase Hosting” kalau mau
  3. Copy config yang muncul

3. Install Firebase SDK

npm install firebase

4. Initialize Firebase

// lib/firebase.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
export default app;
# .env.local
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaxxxxxxxx
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=yourapp.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=yourapp.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abcdef

Note: Firebase API Key aman di-expose ke client karena dibatasi oleh Security Rules. Yang penting jangan expose service account credentials.

Authentication

Firebase Auth support banyak metode autentikasi. Semua udah built-in, tinggal enable di console.

Email/Password

Enable di Firebase Console > Authentication > Sign-in method > Email/Password.

import { 
  createUserWithEmailAndPassword, 
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged 
} from 'firebase/auth';
import { auth } from '@/lib/firebase';

// Sign Up
async function signUp(email, password) {
  try {
    const userCredential = await createUserWithEmailAndPassword(auth, email, password);
    console.log('User created:', userCredential.user);
    return userCredential.user;
  } catch (error) {
    console.error('Sign up error:', error.message);
    throw error;
  }
}

// Sign In
async function signIn(email, password) {
  try {
    const userCredential = await signInWithEmailAndPassword(auth, email, password);
    console.log('User signed in:', userCredential.user);
    return userCredential.user;
  } catch (error) {
    console.error('Sign in error:', error.message);
    throw error;
  }
}

// Sign Out
async function logOut() {
  await signOut(auth);
}

// Listen to Auth State
onAuthStateChanged(auth, (user) => {
  if (user) {
    console.log('User is signed in:', user.uid);
  } else {
    console.log('User is signed out');
  }
});

Google Sign-In

Enable Google provider di Firebase Console, lalu:

import { GoogleAuthProvider, signInWithPopup, signInWithRedirect } from 'firebase/auth';
import { auth } from '@/lib/firebase';

const googleProvider = new GoogleAuthProvider();

// Sign in with popup (desktop-friendly)
async function signInWithGoogle() {
  try {
    const result = await signInWithPopup(auth, googleProvider);
    const user = result.user;
    console.log('Google sign in success:', user.displayName);
    return user;
  } catch (error) {
    console.error('Google sign in error:', error.message);
    throw error;
  }
}

// Sign in with redirect (mobile-friendly)
async function signInWithGoogleRedirect() {
  await signInWithRedirect(auth, googleProvider);
}

Phone Authentication

Enable Phone provider di Firebase Console. Untuk development, tambahkan test phone numbers.

import { 
  RecaptchaVerifier, 
  signInWithPhoneNumber 
} from 'firebase/auth';
import { auth } from '@/lib/firebase';

// Setup reCAPTCHA verifier
function setupRecaptcha(buttonId) {
  window.recaptchaVerifier = new RecaptchaVerifier(auth, buttonId, {
    size: 'invisible',
    callback: () => {
      console.log('reCAPTCHA verified');
    },
  });
}

// Send OTP
async function sendOTP(phoneNumber) {
  setupRecaptcha('sign-in-button');
  const appVerifier = window.recaptchaVerifier;
  
  try {
    const confirmationResult = await signInWithPhoneNumber(auth, phoneNumber, appVerifier);
    window.confirmationResult = confirmationResult;
    console.log('OTP sent!');
  } catch (error) {
    console.error('Error sending OTP:', error.message);
    throw error;
  }
}

// Verify OTP
async function verifyOTP(code) {
  try {
    const result = await window.confirmationResult.confirm(code);
    console.log('Phone auth success:', result.user);
    return result.user;
  } catch (error) {
    console.error('Invalid OTP:', error.message);
    throw error;
  }
}

Auth State dengan React Hook

// hooks/useAuth.js
import { useState, useEffect } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { auth } from '@/lib/firebase';

export function useAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  return { user, loading };
}

Cloud Firestore

Firestore adalah NoSQL document database yang scalable dan support realtime sync. Ini adalah database utama yang recommended untuk most use cases.

Struktur Data

Firestore punya hierarchy: Collections > Documents > Subcollections

users (collection)
  └── userId1 (document)
        ├── name: "Budi"
        ├── email: "[email protected]"
        └── posts (subcollection)
              └── postId1 (document)
                    ├── title: "Hello World"
                    └── content: "..."

CRUD Operations

import { 
  collection, 
  doc, 
  addDoc, 
  setDoc, 
  getDoc, 
  getDocs, 
  updateDoc, 
  deleteDoc,
  query,
  where,
  orderBy,
  limit,
  serverTimestamp 
} from 'firebase/firestore';
import { db } from '@/lib/firebase';

// CREATE - Add document with auto-generated ID
async function createPost(data) {
  const docRef = await addDoc(collection(db, 'posts'), {
    ...data,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });
  console.log('Document written with ID:', docRef.id);
  return docRef.id;
}

// CREATE - Set document with custom ID
async function createUser(userId, data) {
  await setDoc(doc(db, 'users', userId), {
    ...data,
    createdAt: serverTimestamp(),
  });
}

// READ - Get single document
async function getPost(postId) {
  const docSnap = await getDoc(doc(db, 'posts', postId));
  
  if (docSnap.exists()) {
    return { id: docSnap.id, ...docSnap.data() };
  } else {
    console.log('No such document!');
    return null;
  }
}

// READ - Get all documents in collection
async function getAllPosts() {
  const querySnapshot = await getDocs(collection(db, 'posts'));
  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));
}

// UPDATE - Update specific fields
async function updatePost(postId, data) {
  await updateDoc(doc(db, 'posts', postId), {
    ...data,
    updatedAt: serverTimestamp(),
  });
}

// DELETE
async function deletePost(postId) {
  await deleteDoc(doc(db, 'posts', postId));
}

Queries

Firestore support berbagai query operators:

// Query dengan where clause
async function getPublishedPosts() {
  const q = query(
    collection(db, 'posts'),
    where('published', '==', true),
    orderBy('createdAt', 'desc'),
    limit(10)
  );
  
  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));
}

// Multiple where clauses
async function getUserPublishedPosts(userId) {
  const q = query(
    collection(db, 'posts'),
    where('userId', '==', userId),
    where('published', '==', true),
    orderBy('createdAt', 'desc')
  );
  
  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));
}

// Array contains
async function getPostsByTag(tag) {
  const q = query(
    collection(db, 'posts'),
    where('tags', 'array-contains', tag)
  );
  
  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));
}

// In operator - match any of the values
async function getPostsByAuthors(authorIds) {
  const q = query(
    collection(db, 'posts'),
    where('authorId', 'in', authorIds) // Max 10 values
  );
  
  const querySnapshot = await getDocs(q);
  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));
}

Realtime Listeners

Ini yang bikin Firestore powerful—subscribe ke changes secara realtime:

import { onSnapshot } from 'firebase/firestore';

// Listen to single document
function subscribeToPost(postId, callback) {
  const unsubscribe = onSnapshot(doc(db, 'posts', postId), (doc) => {
    if (doc.exists()) {
      callback({ id: doc.id, ...doc.data() });
    }
  });
  
  return unsubscribe; // Call this to stop listening
}

// Listen to query results
function subscribeToPublishedPosts(callback) {
  const q = query(
    collection(db, 'posts'),
    where('published', '==', true),
    orderBy('createdAt', 'desc')
  );
  
  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const posts = querySnapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data(),
    }));
    callback(posts);
  });
  
  return unsubscribe;
}

// React hook untuk realtime data
function usePosts() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const q = query(
      collection(db, 'posts'),
      where('published', '==', true),
      orderBy('createdAt', 'desc')
    );

    const unsubscribe = onSnapshot(q, (snapshot) => {
      const data = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data(),
      }));
      setPosts(data);
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  return { posts, loading };
}

Batch Writes dan Transactions

import { writeBatch, runTransaction } from 'firebase/firestore';

// Batch write - multiple operations atomically
async function batchUpdate(updates) {
  const batch = writeBatch(db);
  
  updates.forEach(({ id, data }) => {
    const docRef = doc(db, 'posts', id);
    batch.update(docRef, data);
  });
  
  await batch.commit();
}

// Transaction - read-then-write atomically
async function incrementLikeCount(postId) {
  await runTransaction(db, async (transaction) => {
    const postRef = doc(db, 'posts', postId);
    const postDoc = await transaction.get(postRef);
    
    if (!postDoc.exists()) {
      throw new Error('Post does not exist!');
    }
    
    const newLikeCount = (postDoc.data().likeCount || 0) + 1;
    transaction.update(postRef, { likeCount: newLikeCount });
  });
}

Cloud Storage

Firebase Storage untuk upload dan manage files seperti images, videos, documents.

Upload Files

import { 
  ref, 
  uploadBytes, 
  uploadBytesResumable,
  getDownloadURL,
  deleteObject 
} from 'firebase/storage';
import { storage } from '@/lib/firebase';

// Simple upload
async function uploadFile(file, path) {
  const storageRef = ref(storage, path);
  const snapshot = await uploadBytes(storageRef, file);
  const downloadURL = await getDownloadURL(snapshot.ref);
  return downloadURL;
}

// Upload dengan progress tracking
function uploadFileWithProgress(file, path, onProgress) {
  return new Promise((resolve, reject) => {
    const storageRef = ref(storage, path);
    const uploadTask = uploadBytesResumable(storageRef, file);
    
    uploadTask.on('state_changed',
      (snapshot) => {
        const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        onProgress(progress);
      },
      (error) => {
        reject(error);
      },
      async () => {
        const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
        resolve(downloadURL);
      }
    );
  });
}

// Delete file
async function deleteFile(path) {
  const storageRef = ref(storage, path);
  await deleteObject(storageRef);
}

Upload Avatar Example

async function uploadAvatar(userId, file) {
  // Validate file
  if (!file.type.startsWith('image/')) {
    throw new Error('File must be an image');
  }
  
  if (file.size > 5 * 1024 * 1024) { // 5MB limit
    throw new Error('File size must be less than 5MB');
  }
  
  // Create unique filename
  const extension = file.name.split('.').pop();
  const filename = `avatars/${userId}/avatar.${extension}`;
  
  // Upload
  const downloadURL = await uploadFile(file, filename);
  
  // Update user profile
  await updateDoc(doc(db, 'users', userId), {
    avatarURL: downloadURL,
  });
  
  return downloadURL;
}

Cloud Functions

Cloud Functions untuk server-side logic. Cocok untuk webhooks, background processing, atau sensitive operations.

Setup Cloud Functions

npm install -g firebase-tools
firebase login
firebase init functions

Pilih TypeScript atau JavaScript, lalu install dependencies.

Function Examples

// functions/src/index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';

admin.initializeApp();
const db = admin.firestore();

// HTTP Function
export const helloWorld = functions.https.onRequest((req, res) => {
  res.json({ message: 'Hello from Firebase!' });
});

// Firestore Trigger - on document create
export const onUserCreated = functions.firestore
  .document('users/{userId}')
  .onCreate(async (snap, context) => {
    const user = snap.data();
    const userId = context.params.userId;
    
    // Send welcome email, create default data, etc.
    await db.collection('notifications').add({
      userId,
      type: 'welcome',
      message: `Welcome ${user.name}!`,
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
    });
  });

// Firestore Trigger - on document update
export const onPostUpdated = functions.firestore
  .document('posts/{postId}')
  .onUpdate(async (change, context) => {
    const before = change.before.data();
    const after = change.after.data();
    
    // Check if post was just published
    if (!before.published && after.published) {
      // Send notifications to followers
      console.log('Post published:', context.params.postId);
    }
  });

// Auth Trigger - on user create
export const onAuthUserCreated = functions.auth.user().onCreate(async (user) => {
  // Create user profile in Firestore
  await db.collection('users').doc(user.uid).set({
    email: user.email,
    displayName: user.displayName || '',
    photoURL: user.photoURL || '',
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
  });
});

// Scheduled Function (Cron)
export const dailyCleanup = functions.pubsub
  .schedule('every 24 hours')
  .onRun(async (context) => {
    // Delete old notifications
    const thirtyDaysAgo = new Date();
    thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
    
    const oldNotifications = await db
      .collection('notifications')
      .where('createdAt', '<', thirtyDaysAgo)
      .get();
    
    const batch = db.batch();
    oldNotifications.docs.forEach(doc => batch.delete(doc.ref));
    await batch.commit();
    
    console.log(`Deleted ${oldNotifications.size} old notifications`);
  });

// Callable Function (called from client SDK)
export const processPayment = functions.https.onCall(async (data, context) => {
  // Check authentication
  if (!context.auth) {
    throw new functions.https.HttpsError('unauthenticated', 'User must be logged in');
  }
  
  const { amount, currency } = data;
  
  // Process payment logic here
  // ...
  
  return { success: true, transactionId: 'xxx' };
});

Deploy Functions

firebase deploy --only functions

Call Callable Function from Client

import { getFunctions, httpsCallable } from 'firebase/functions';

const functions = getFunctions();
const processPayment = httpsCallable(functions, 'processPayment');

async function handlePayment() {
  try {
    const result = await processPayment({ amount: 100000, currency: 'IDR' });
    console.log('Payment result:', result.data);
  } catch (error) {
    console.error('Payment error:', error.message);
  }
}

Firebase Hosting

Hosting untuk deploy web apps dengan CDN global.

Setup Hosting

firebase init hosting

Pilih public directory (biasanya out untuk Next.js static export atau dist untuk Vite).

Deploy

# Build project
npm run build

# Deploy
firebase deploy --only hosting

Custom Domain

  1. Buka Firebase Console > Hosting
  2. Klik “Add custom domain”
  3. Ikuti instruksi untuk verify domain
  4. Update DNS records sesuai instruksi
  5. Tunggu SSL certificate provisioning (biasanya beberapa menit)

Security Rules

Security Rules adalah firewall untuk Firestore dan Storage. Ini WAJIB di-configure untuk production.

Firestore Rules

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    // Helper functions
    function isAuthenticated() {
      return request.auth != null;
    }
    
    function isOwner(userId) {
      return request.auth.uid == userId;
    }
    
    function isValidPost() {
      return request.resource.data.keys().hasAll(['title', 'content', 'userId'])
        && request.resource.data.title is string
        && request.resource.data.title.size() > 0
        && request.resource.data.title.size() <= 200;
    }
    
    // Users collection
    match /users/{userId} {
      allow read: if true; // Public profiles
      allow create: if isAuthenticated() && isOwner(userId);
      allow update: if isOwner(userId);
      allow delete: if false; // Prevent accidental deletion
    }
    
    // Posts collection
    match /posts/{postId} {
      allow read: if resource.data.published == true || isOwner(resource.data.userId);
      allow create: if isAuthenticated() 
        && isOwner(request.resource.data.userId)
        && isValidPost();
      allow update: if isOwner(resource.data.userId);
      allow delete: if isOwner(resource.data.userId);
    }
    
    // Comments subcollection
    match /posts/{postId}/comments/{commentId} {
      allow read: if true;
      allow create: if isAuthenticated();
      allow update, delete: if isOwner(resource.data.userId);
    }
  }
}

Storage Rules

// storage.rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    
    // Avatars - user can only access their own
    match /avatars/{userId}/{fileName} {
      allow read: if true; // Public avatars
      allow write: if request.auth.uid == userId
        && request.resource.size < 5 * 1024 * 1024 // Max 5MB
        && request.resource.contentType.matches('image/.*');
    }
    
    // Post images
    match /posts/{postId}/{fileName} {
      allow read: if true;
      allow write: if request.auth != null
        && request.resource.size < 10 * 1024 * 1024 // Max 10MB
        && request.resource.contentType.matches('image/.*');
    }
    
    // Private documents
    match /documents/{userId}/{fileName} {
      allow read, write: if request.auth.uid == userId;
    }
  }
}

Deploy Rules

firebase deploy --only firestore:rules
firebase deploy --only storage:rules

Integrasi dengan React/Next.js

Context Provider Pattern

// context/AuthContext.js
import { createContext, useContext, useEffect, useState } from 'react';
import { onAuthStateChanged } from 'firebase/auth';
import { doc, getDoc } from 'firebase/firestore';
import { auth, db } from '@/lib/firebase';

const AuthContext = createContext({});

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [userProfile, setUserProfile] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, async (user) => {
      if (user) {
        setUser(user);
        
        // Fetch user profile from Firestore
        const profileDoc = await getDoc(doc(db, 'users', user.uid));
        if (profileDoc.exists()) {
          setUserProfile({ id: profileDoc.id, ...profileDoc.data() });
        }
      } else {
        setUser(null);
        setUserProfile(null);
      }
      setLoading(false);
    });

    return () => unsubscribe();
  }, []);

  return (
    <AuthContext.Provider value={{ user, userProfile, loading }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

Protected Route Component

// components/ProtectedRoute.js
import { useAuth } from '@/context/AuthContext';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();
  const router = useRouter();

  useEffect(() => {
    if (!loading && !user) {
      router.push('/login');
    }
  }, [user, loading, router]);

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return null;
  }

  return children;
}

Data Fetching dengan SWR

// hooks/usePosts.js
import useSWR from 'swr';
import { collection, getDocs, query, where, orderBy } from 'firebase/firestore';
import { db } from '@/lib/firebase';

async function fetchPosts() {
  const q = query(
    collection(db, 'posts'),
    where('published', '==', true),
    orderBy('createdAt', 'desc')
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
  }));
}

export function usePosts() {
  const { data, error, isLoading, mutate } = useSWR('posts', fetchPosts);
  
  return {
    posts: data || [],
    isLoading,
    error,
    refresh: mutate,
  };
}

Tips Optimasi Pricing

Firebase punya free tier (Spark Plan) yang generous, tapi bisa mahal kalau nggak hati-hati. Berikut tips untuk optimize cost:

1. Minimalisir Reads

// ❌ Bad: Read semua field
const doc = await getDoc(doc(db, 'users', userId));
const name = doc.data().name;

// ✅ Good: Gunakan subcollection untuk data yang sering berubah
// atau pisahkan data yang jarang diakses ke dokumen terpisah

2. Gunakan Pagination

import { startAfter, limit } from 'firebase/firestore';

async function getPaginatedPosts(lastDoc, pageSize = 10) {
  let q = query(
    collection(db, 'posts'),
    orderBy('createdAt', 'desc'),
    limit(pageSize)
  );
  
  if (lastDoc) {
    q = query(q, startAfter(lastDoc));
  }
  
  const snapshot = await getDocs(q);
  return {
    posts: snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })),
    lastDoc: snapshot.docs[snapshot.docs.length - 1],
  };
}

3. Cache Data di Client

// Gunakan SWR atau React Query untuk caching
// Data nggak perlu di-fetch ulang setiap render

4. Optimasi Storage

  • Compress images sebelum upload
  • Delete files yang nggak terpakai
  • Set lifecycle policies untuk auto-delete old files

5. Gunakan Blaze Plan untuk Production

Blaze Plan (pay-as-you-go) lebih fleksibel dan bisa set budget alerts.

Pricing Comparison

FeatureSpark (Free)Blaze (Pay-as-you-go)
Firestore Reads50K/day$0.06/100K
Firestore Writes20K/day$0.18/100K
Storage5GB$0.026/GB
Hosting10GB/month$0.15/GB
Functions Invocations125K/month$0.40/million
AuthUnlimitedUnlimited

Firebase vs Supabase

Ini pertanyaan yang sering muncul. Keduanya punya kelebihan masing-masing:

Pilih Firebase kalau:

  • Butuh ekosistem Google yang terintegrasi (Analytics, Crashlytics, A/B Testing)
  • Target mobile-first (SDK lebih mature untuk iOS/Android)
  • Prefer NoSQL dan familiar dengan document-based database
  • Butuh fitur seperti Remote Config, Dynamic Links
  • Tim udah familiar dengan Firebase

Pilih Supabase kalau:

  • Prefer SQL dan relational database (PostgreSQL)
  • Butuh full database access dan complex queries
  • Mau opsi self-hosting
  • Concern tentang vendor lock-in
  • Butuh features seperti Row Level Security yang lebih granular

Feature Comparison

FeatureFirebaseSupabase
DatabaseNoSQL (Firestore)PostgreSQL
RealtimeYesYes
AuthBuilt-inBuilt-in
StorageYesYes
FunctionsCloud FunctionsEdge Functions (Deno)
PricingPay-as-you-goFree tier + Pro
Self-hostNoYes
Vendor lock-inHigherLower
Mobile SDKExcellentGood
Open sourceNoYes

Honestly, keduanya solid choices. Pilih berdasarkan kebutuhan project dan preferensi tim.

Best Practices

1. Struktur Data yang Baik

// ❌ Bad: Nested data yang dalam
{
  user: {
    posts: {
      post1: {
        comments: { ... }
      }
    }
  }
}

// ✅ Good: Flat structure dengan references
// users/{userId}
// posts/{postId} dengan field userId
// comments/{commentId} dengan field postId

2. Index Firestore Queries

Firestore akan error kalau query butuh composite index yang belum ada. Klik link di error message untuk auto-create index.

3. Handle Offline Mode

import { enableIndexedDbPersistence } from 'firebase/firestore';

// Enable offline persistence
enableIndexedDbPersistence(db).catch((err) => {
  if (err.code === 'failed-precondition') {
    console.log('Multiple tabs open');
  } else if (err.code === 'unimplemented') {
    console.log('Browser doesn\'t support persistence');
  }
});

4. Error Handling yang Proper

async function fetchData() {
  try {
    const data = await getDoc(doc(db, 'posts', postId));
    if (!data.exists()) {
      throw new Error('Post not found');
    }
    return data.data();
  } catch (error) {
    // Log untuk debugging
    console.error('Fetch error:', error);
    
    // Return user-friendly error
    if (error.code === 'permission-denied') {
      throw new Error('Kamu tidak punya akses ke data ini');
    }
    throw new Error('Gagal mengambil data. Coba lagi nanti.');
  }
}

5. Environment Variables

Jangan hardcode config. Gunakan environment variables dan jangan commit .env ke git.

Kesimpulan

Firebase adalah platform yang powerful untuk rapid development. Dengan ekosistem yang lengkap dari Google, kamu bisa fokus ke building features daripada setup infrastructure.

Key takeaways:

  • Authentication - Multiple providers, easy to implement
  • Firestore - Flexible NoSQL dengan realtime sync
  • Cloud Functions - Serverless backend logic
  • Security Rules - Wajib di-configure untuk production
  • Pricing - Gratis untuk start, tapi perlu dimonitor

Start dengan free tier untuk learning dan prototyping. Upgrade ke Blaze Plan kalau udah ready untuk production dan butuh more resources.

Dokumentasi Firebase sangat lengkap di firebase.google.com/docs. Happy building!