Scaffold a production-ready Firebase project: Firestore data modeling, Security Rules, Cloud Functions v2, emulator suite, hosting config, and CI/CD. Covers auth flows, subcollection design, and multi-environment strategy.
The canonical home for this skill is scaffold-firebase-project in JaviMontano/mao-sovereign-architect
"Security Rules are your server-side logic — treat them with the same rigor as application code."
Five-step procedure to generate a Firebase project with correct Firestore data modeling, tight Security Rules, typed Cloud Functions v2, local emulator integration, and a CI/CD pipeline that validates rules before every deploy.
firebase.json, .firebaserc, firestore.rules,
firestore.indexes.json, existing functions/ directory.[HECHO] for confirmed, [INFERENCIA] for derived, [SUPUESTO] for assumed.Firestore is a document database — model for your queries, not for normalization.
| Signal | Embed (subcollection / nested map) | Reference (separate collection) |
|---|---|---|
| Data always read together | YES | NO |
| Unbounded growth | NO | YES |
| Need to query across parent docs | NO | YES |
| Data shared by multiple parents | NO | YES |
| Update frequency differs | NO | YES |
posts/{postId} create).FieldValue.increment() — never read-count-write cycles./stats/{userId} doc updated by Functions.collectionGroup index in firestore.indexes.json.// firestore.indexes.json — always commit this file
{
"indexes": [
{
"collectionGroup": "orders",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "userId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
],
"fieldOverrides": []
}Security Rules run on every read/write — they are your authorization layer.
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions — reuse, don't repeat
function isSignedIn() {
return request.auth != null;
}
function isOwner(userId) {
return isSignedIn() && request.auth.uid == userId;
}
function hasRole(role) {
return isSignedIn() &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == role;
}
function validUserFields() {
return request.resource.data.keys().hasOnly(['displayName','email','updatedAt'])
&& request.resource.data.displayName is string
&& request.resource.data.displayName.size() <= 100;
}
match /users/{userId} {
allow read: if isOwner(userId) || hasRole('admin');
allow create: if isOwner(userId) && validUserFields();
allow update: if isOwner(userId) && validUserFields();
allow delete: if hasRole('admin');
}
match /posts/{postId} {
allow read: if resource.data.published == true || isOwner(resource.data.authorId);
allow create: if isSignedIn()
&& request.resource.data.authorId == request.auth.uid
&& request.resource.data.title.size() <= 200;
allow update: if isOwner(resource.data.authorId);
allow delete: if isOwner(resource.data.authorId) || hasRole('moderator');
}
}
}# Always test rules before deploy
firebase emulators:exec --only firestore \
"npx jest --testPathPattern=firestore.rules.test"Use functions/v2 for all new projects — better concurrency, min instances, and pricing.
// functions/src/index.ts
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
import { onCall, HttpsError } from 'firebase-functions/v2/https';
import { onSchedule } from 'firebase-functions/v2/scheduler';
import * as admin from 'firebase-admin';
admin.initializeApp();
// Triggered function — fan-out on new post
export const onPostCreated = onDocumentCreated(
{ document: 'posts/{postId}', region: 'us-central1' },
async (event) => {
const post = event.data?.data();
if (!post) return;
// Fan-out to followers...
}
);
// Callable function — server-side validation
export const createCheckoutSession = onCall(
{ region: 'us-central1', enforceAppCheck: true },
async (request) => {
if (!request.auth) throw new HttpsError('unauthenticated', 'Login required');
// Business logic...
return { sessionId: 'cs_...' };
}
);
// Scheduled — daily cleanup
export const dailyCleanup = onSchedule('every 24 hours', async () => {
const cutoff = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
// Delete stale docs...
});// firebase.json
{
"functions": [{ "source": "functions", "codebase": "default" }],
"firestore": {
"rules": "firestore.rules",
"indexes": "firestore.indexes.json"
},
"hosting": {
"public": "dist",
"rewrites": [{ "source": "**", "destination": "/index.html" }],
"headers": [
{
"source": "**/*.@(js|css)",
"headers": [{ "key": "Cache-Control", "value": "max-age=31536000" }]
}
]
}
}// firebase.json — emulators block
{
"emulators": {
"auth": { "port": 9099 },
"firestore": { "port": 8080 },
"functions": { "port": 5001 },
"hosting": { "port": 5000 },
"storage": { "port": 9199 },
"ui": { "enabled": true, "port": 4000 }
}
}// src/lib/firebase.ts — environment-aware init
import { connectFirestoreEmulator, getFirestore } from 'firebase/firestore';
import { connectAuthEmulator, getAuth } from 'firebase/auth';
const db = getFirestore();
const auth = getAuth();
if (import.meta.env.DEV) {
connectFirestoreEmulator(db, '127.0.0.1', 8080);
connectAuthEmulator(auth, 'http://127.0.0.1:9099');
}# .github/workflows/firebase.yml
name: Firebase CI
on: [push]
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: npm ci && cd functions && npm ci
- run: npx firebase-tools emulators:exec --only firestore,auth
"npx jest --passWithNoTests"
env: { FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} }
- run: npm run build
- run: npx firebase-tools deploy --only firestore,functions,hosting
if: github.ref == 'refs/heads/main'
env: { FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} }firestore.indexes.json in version control, no composite index surprises in prod.any in function signatures..firebaserc with dev, staging, prod aliases.request.resource.data validated for type and length.enforceAppCheck: true in prod.get() in rules counts as a read and adds latency — use custom claims instead for hot paths).allow read, write: if true and planning to "fix it later" — this has caused production data leaks.users collection with all data — model subcollections for logical groupings, queries, and security boundaries.b56591e
Canonical home
since Aug 28, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.