tool
Firebase
Firebase
Google’s managed BaaS — Firestore (document DB), Auth, Storage, Functions, Hosting. Two SDKs: Web SDK (client, public-rules-gated) and Admin SDK (server, full-trust). The user’s default backend is Supabase, but Firebase has its place; this page captures when and how.
When Firebase wins
- Real-time out of the box —
onSnapshot()is one line; Supabase realtime is a separate channel setup - Google identity — Google sign-in is trivial; Apple/email/phone too
- Storage rules in plain language — security rules DSL is more ergonomic than Postgres RLS for “user X owns object Y”
- No Postgres maintenance — fully managed, no migrations, schema-on-read
- Wide language SDKs — official Admin SDKs for Node, Go, Python, Java, .NET
When Firebase loses
- Schema-on-read is its sin: no migrations, no relational integrity, no joins. You denormalise or you die.
- Firestore queries are weak — single-field where + range only, compound indexes per query shape, no
ORuntil recently and still awkward - Cost shape — pricing is per-document-read; chatty UIs surprise you
- Vendor lock-in — moving off Firestore is a rewrite; moving off Postgres is a migration script
- No SQL — no ad-hoc queries, no BI tooling without exporting to BigQuery
Drizzle ORM + Supabase is the user’s default for new projects for these reasons.
Server vs client SDKs
// Server (Admin SDK) — full trust, runs on the server only
import { initializeApp, cert } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
initializeApp({ credential: cert(serviceAccountJson) })
const db = getFirestore() // bypasses all security rules
// Client (Web SDK) — security-rules-gated, runs in the browser
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
initializeApp({ apiKey, authDomain, projectId, ... }) // public config
const db = getFirestore() // every read/write checked by security rules
Two import paths, two db types — same shape but different methods. Easy to confuse. The Web SDK uses functional API (collection(db, 'orders')) while Admin SDK uses method chains (db.collection('orders')).
Auth — session cookies are the right pattern
Storing the Firebase ID token in localStorage works but is the wrong default in modern Next.js:
- ID token is exposed to JS → XSS risk
- Server can’t read it without a request round-trip
- Token expiry is 1 hour, refresh dance is finicky
Better: trade the ID token for a server-managed session cookie via Admin SDK:
// Server Action: client posts ID token, server returns Set-Cookie
const sessionCookie = await auth.createSessionCookie(idToken, { expiresIn: 5 * 24 * 3600 * 1000 })
cookies().set('session', sessionCookie, { httpOnly: true, secure: true, sameSite: 'lax' })
// Middleware: verify on every request
const decoded = await auth.verifySessionCookie(cookie, true)
Used in kulevents — admin login flow.
Env-aware Firestore collections
Cheap dev/prod split without separate Firebase projects:
const env = process.env.FIREBASE_ENV ?? 'development'
const suffix = env === 'production' ? '' : '-dev'
export const COLLECTIONS = {
ORDERS: `orders${suffix}`,
LAYOUTS: `layouts${suffix}`,
// ...
}
A single project hosts both, queries pick the right collection name, security rules cover both. Used in kulevents (lib/firebase/config.ts). Fine for a single-developer project; for proper isolation use separate Firebase projects.
Used in
- kulevents — Firestore (orders, layouts, templates), Storage (template images, custom-design results), Auth (admin only). First Firebase project in the kulify ecosystem.
See also
- Supabase — user’s preferred backend (Postgres + Auth + Storage + Realtime + Edge Functions)
- Drizzle ORM — preferred ORM when on Postgres
- kulevents — production usage