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.
From sanpx claudepluginhub javimontano/jm-adk --plugin sovereign-architectThis skill is limited to using the following tools:
agents/firestore-security-rules-agent.mdagents/scaffold-firebase-project-agent.mdevals/evals.jsonexamples/sample-output.mdprompts/use-case-prompts.mdreferences/body-of-knowledge.mdreferences/knowledge-graph.mmdreferences/state-of-the-art.md"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.Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.