// Comprehensive Firebase development guidance covering project setup, feature development, debugging, and validation. Auto-detects intent and routes to specialized sub-skills. Patterns extracted from production projects.
| name | firebase-development |
| description | Comprehensive Firebase development guidance covering project setup, feature development, debugging, and validation. Auto-detects intent and routes to specialized sub-skills. Patterns extracted from production projects. |
This skill system guides Firebase development using proven patterns from production projects. It covers:
This skill will invoke one of four sub-skills:
firebase-development:project-setup - Initialize new Firebase projectsfirebase-development:add-feature - Add functions/collections/endpointsfirebase-development:debug - Troubleshoot emulator and runtime issuesfirebase-development:validate - Review Firebase code for security/patternsThe main skill detects your intent and routes to the appropriate sub-skill. All sub-skills reference shared Firebase patterns documented in this file.
Use this skill system when working with Firebase projects:
Patterns are extracted from three production Firebase projects:
/Users/dylanr/work/2389/oneonone): Express API architecture, custom API keys, server-write-only security/Users/dylanr/work/2389/bot-socialmedia-server): Domain-grouped functions, Firebase Auth + roles, client-write with validation/Users/dylanr/work/2389/meme-rodeo): Individual function files, Firebase Auth + entitlementsThese projects demonstrate different valid approaches to Firebase architecture. The skills help you choose the right pattern for your needs.
This skill uses keyword-based routing to determine which sub-skill to use:
project-setup:
add-feature:
debug:
validate:
If intent is unclear, ask:
Question: "What Firebase task are you working on?"
Options:
- "Project Setup" (Initialize new Firebase project)
- "Add Feature" (Add functions, collections, endpoints)
- "Debug Issue" (Troubleshoot errors or problems)
- "Validate Code" (Review against patterns)
These patterns are documented once here and referenced by all sub-skills. Choose the approach that fits your project needs.
Firebase supports multiple hosting configurations. Choose based on your needs:
site: Based (Preferred for Simplicity)When to use: Multiple independent deployments with separate URLs
Configuration:
{
"hosting": [
{
"site": "oneonone-mcp",
"source": "hosting",
"frameworksBackend": {"region": "us-central1"}
},
{
"site": "oneonone-mcp-mcp",
"public": "hosting-mcp",
"rewrites": [{"source": "/**", "function": "mcpEndpoint"}]
},
{
"site": "oneonone-mcp-api",
"public": "hosting-api",
"rewrites": [{"source": "/**", "function": "mcpEndpoint"}]
}
]
}
Setup:
# Create sites in Firebase Console or via CLI
firebase hosting:sites:create oneonone-mcp
firebase hosting:sites:create oneonone-mcp-mcp
firebase hosting:sites:create oneonone-mcp-api
# Deploy specific site
firebase deploy --only hosting:oneonone-mcp
# Deploy all hosting sites
firebase deploy --only hosting
URLs: Each site gets its own URL: oneonone-mcp.web.app, oneonone-mcp-mcp.web.app, oneonone-mcp-api.web.app
Emulator Testing: When using emulators, all sites are served on the same hosting port (5000). Test one site at a time or use paths to differentiate.
Benefits:
Note: predeploy hooks are not supported with site-based configs. Use target-based (Option 2) if you need build scripts before deployment.
Example: oneonone uses 3 separate sites (main app, MCP endpoint, API endpoint)
Reference: /Users/dylanr/work/2389/oneonone/firebase.json
target: Based (Use for Predeploy Hooks)When to use: Need build scripts before deployment, monorepo coordination
Configuration:
{
"hosting": [
{
"target": "main",
"source": "hosting",
"frameworksBackend": {"region": "us-central1"}
},
{
"target": "streamer",
"public": "streamer",
"predeploy": ["cd streamer-app && npm install", "cd streamer-app && npm run build"]
},
{
"target": "api",
"public": "api",
"rewrites": [
{"source": "/api**/**", "function": "api"}
]
}
]
}
Setup:
# Link targets to sites (one-time setup stored in .firebaserc)
firebase target:apply hosting main bot-socialmedia-main
firebase target:apply hosting streamer bot-socialmedia-streamer
firebase target:apply hosting api bot-socialmedia-api
# Deploy specific target (runs predeploy hooks)
firebase deploy --only hosting:main
# Deploy all targets
firebase deploy --only hosting
Emulator Configuration with Targets:
{
"emulators": {
"hosting": [
{"target": "main", "port": 5000},
{"target": "api", "port": 5002}
]
}
}
Benefits:
Trade-offs:
.firebaserc (must track this file)Example: bot-socialmedia uses targets with predeploy hooks for builds
Reference: /Users/dylanr/work/2389/bot-socialmedia-server/firebase.json
When to use: Smaller projects, all content under one domain
Configuration:
{
"hosting": {
"source": "hosting",
"site": "rodeo-meme",
"frameworksBackend": {"region": "us-central1"},
"rewrites": [
{
"source": "/images/memes/**",
"function": {
"functionId": "proxyMemeFile",
"region": "us-central1"
}
}
]
}
}
Setup:
# No special setup needed - just deploy
firebase deploy --only hosting
Benefits:
Trade-offs:
Example: meme-rodeo uses single hosting with function rewrites
Reference: /Users/dylanr/work/2389/meme-rodeo/firebase.json
site: if: You need multiple independent URLs, straightforward deploymenttarget: if: You need predeploy build scripts, monorepo patternsMigration path: Start with single hosting, migrate to site: based when you need multiple URLs
Firebase projects can use custom API keys, Firebase Auth, or both. Choose based on your use case.
When to use:
Format: {projectPrefix}_ + unique identifier
ooo_abc123 (OneOnOne), meme_xyz789 (Meme Rodeo), bot_def456 (Bot Social)Storage Pattern:
/users/{userId}/apiKeys/{keyId}
- keyId: "ooo_abc123..." (the actual key)
- userId: string
- active: boolean
- createdAt: timestamp
Note: The keyId field contains the actual API key and enables collection group queries (used by middleware to find keys across all users).
Middleware Pattern:
// functions/src/middleware/apiKeyGuard.ts
import { Request, Response, NextFunction } from 'express';
import * as admin from 'firebase-admin';
export async function apiKeyGuard(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'] as string;
if (!apiKey || !apiKey.startsWith('ooo_')) { // Replace 'ooo_' with your project prefix
res.status(401).json({ error: 'Invalid API key' });
return;
}
const db = admin.firestore();
const apiKeysQuery = await db
.collectionGroup('apiKeys')
.where('keyId', '==', apiKey)
.where('active', '==', true)
.limit(1)
.get();
if (apiKeysQuery.empty) {
res.status(401).json({ error: 'Invalid API key' });
return;
}
req.userId = apiKeysQuery.docs[0].data().userId;
next();
}
Example: oneonone's API key implementation
Reference: /Users/dylanr/work/2389/oneonone/functions/src/middleware/apiKeyGuard.ts
When to use:
Role Storage:
/users/{userId}
- role: "admin" | "teamlead" | "user" (or)
- entitlement: "admin" | "moderator" | "public" | "waitlist"
- displayName: string
- email: string
Firestore Rules Pattern:
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
function isModerator() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.entitlement == 'moderator';
}
match /users/{userId} {
allow read: if request.auth != null && (request.auth.uid == userId || isAdmin());
allow update: if request.auth != null && request.auth.uid == userId;
}
Examples:
role field (admin/teamlead/user)entitlement field (admin/moderator/public/waitlist)References:
/Users/dylanr/work/2389/bot-socialmedia-server/firestore.rules/Users/dylanr/work/2389/meme-rodeo/firestore.rulesPattern: Firebase Auth for web UI + API keys for programmatic access
Example Use Case:
Implementation:
request.auth.uidx-api-key header via middlewareChoose an architecture pattern based on your project type. All patterns work with hosting rewrites for API routing.
When to use:
Structure:
functions/src/
├── index.ts # Express app export
├── middleware/
│ ├── apiKeyGuard.ts
│ └── loggingMiddleware.ts
├── tools/ # Or "routes/"
│ ├── requestSession.ts
│ ├── sendMessage.ts
│ └── endSession.ts
├── services/
│ └── sessionManager.ts
└── shared/
├── types.ts
└── config.ts
index.ts Pattern:
// ABOUTME: Main entry point for Firebase Functions - exports MCP endpoint with tool routing
// ABOUTME: Configures Express app with authentication, CORS, and health check
import * as admin from 'firebase-admin';
import { onRequest } from 'firebase-functions/v2/https';
import express, { Request, Response } from 'express';
import cors from 'cors';
import { apiKeyGuard } from './middleware/apiKeyGuard';
import { handleRequestSession } from './tools/requestSession';
admin.initializeApp();
const app = express();
app.use(cors({ origin: true }));
app.use(express.json());
app.get('/health', (_req, res) => {
res.status(200).json({ status: 'ok' });
});
app.post('/mcp', apiKeyGuard, async (req, res) => {
const { tool, params } = req.body;
const userId = req.userId!;
let result;
switch (tool) {
case 'request_session':
result = await handleRequestSession(userId, params);
break;
default:
res.status(400).json({ success: false, error: 'Unknown tool' });
return;
}
res.status(200).json(result);
});
export const mcpEndpoint = onRequest({ invoker: 'public', cors: true }, app);
Hosting Rewrite (firebase.json):
{
"hosting": {
"rewrites": [
{"source": "/**", "function": "mcpEndpoint"}
]
}
}
Example: oneonone uses Express routing for MCP tools
Reference: /Users/dylanr/work/2389/oneonone/functions/src/index.ts
When to use:
Structure:
functions/src/
├── index.ts # Re-exports all functions
├── posts.ts # All post-related functions
├── journal.ts # All journal functions
├── admin.ts # Admin functions
├── teamSummaries.ts # Summary generation
└── shared/
├── types/
├── validators/
└── utils/
Domain File Pattern (posts.ts):
// ABOUTME: Post creation, reading, and management functions
// ABOUTME: Includes API endpoints and real-time triggers
import { onRequest } from 'firebase-functions/v2/https';
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
export const createPost = onRequest(async (req, res) => {
// Implementation
});
export const getPosts = onRequest(async (req, res) => {
// Implementation
});
export const onPostCreated = onDocumentCreated('teams/{teamId}/posts/{postId}', async (event) => {
// Trigger implementation
});
index.ts Pattern:
// ABOUTME: Main entry point - re-exports all Cloud Functions
// ABOUTME: Organizes functions by domain for clear structure
export * from './posts';
export * from './journal';
export * from './admin';
export * from './teamSummaries';
Example: bot-socialmedia uses domain-grouped architecture
Reference: /Users/dylanr/work/2389/bot-socialmedia-server/functions/src/
When to use:
Structure:
functions/
├── index.js # Imports and exports all
└── functions/
├── upload.js
├── searchMemes.js
├── generateInvite.js
├── onFileUploaded.js
└── periodicFileCheck.js
Function File Pattern:
const { onRequest } = require('firebase-functions/v2/https');
exports.upload = onRequest(async (req, res) => {
// Implementation
});
index.js Pattern:
const { upload } = require("./functions/upload");
const { searchMemes } = require("./functions/searchMemes");
exports.upload = upload;
exports.searchMemes = searchMemes;
Example: meme-rodeo uses individual function files
Reference: /Users/dylanr/work/2389/meme-rodeo/functions/
Choose a security philosophy based on your write patterns.
When to use:
Pattern:
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// All collections: read allowed, write denied
match /config/{configId} {
allow read: if true;
allow write: if false; // Only Cloud Functions can write
}
match /users/{userId} {
allow read: if isOwner(userId);
allow write: if false; // Only Cloud Functions can write
// User API keys subcollection
match /apiKeys/{keyId} {
allow read: if isOwner(userId);
allow write: if false; // Only Cloud Functions can write
}
}
// Default deny
match /{document=**} {
allow read, write: if false;
}
}
}
Benefits:
Trade-offs:
Example: oneonone uses server-write-only exclusively
Reference: /Users/dylanr/work/2389/oneonone/firestore.rules
When to use:
Pattern:
// firestore.rules
match /teams/{teamId}/posts/{postId} {
// Allow users to create their own posts
allow create: if request.auth != null &&
request.resource.data.userId == request.auth.uid &&
request.resource.data.teamId == teamId;
// Allow users to update only specific fields of their posts
allow update: if request.auth != null &&
resource.data.userId == request.auth.uid &&
request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['content', 'tags', 'updatedAt']);
// Allow users to delete their own posts
allow delete: if request.auth != null &&
resource.data.userId == request.auth.uid;
}
Benefits:
Trade-offs:
Examples:
References:
/Users/dylanr/work/2389/bot-socialmedia-server/firestore.rules/Users/dylanr/work/2389/meme-rodeo/firestore.rulesCommon patterns used across all projects.
Always extract reusable logic into functions:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Authentication helpers
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return isAuthenticated() && request.auth.uid == userId;
}
// Role-based helpers
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
function isTeamMember(teamId, userId) {
let team = get(/databases/$(database)/documents/teams/$(teamId)).data;
return team != null && team.members.hasAny([{'uid': userId}]);
}
// Use helpers in rules
match /users/{userId} {
allow read: if isOwner(userId) || isAdmin();
allow write: if isOwner(userId);
}
}
}
Benefits:
All three projects use this pattern heavily
Use when allowing client writes to restrict which fields can change:
// Only allow updating specific safe fields
allow update: if request.auth != null &&
request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['displayName', 'bio', 'photoURL']);
// Prevent privilege escalation
allow update: if request.auth != null &&
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin' &&
request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['role', 'updatedAt']);
Example: bot-socialmedia uses extensively to protect sensitive fields
Reference: /Users/dylanr/work/2389/bot-socialmedia-server/firestore.rules:22
Look up user roles from /users collection:
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
function hasEntitlement(level) {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.entitlement == level;
}
match /files/{fileId} {
allow read: if true;
allow delete: if request.auth != null && isAdmin();
allow update: if request.auth != null && (isAdmin() || hasEntitlement('moderator'));
}
Examples:
role fieldentitlement fieldAdd separate rules when using collectionGroup() queries:
// Regular collection rules
match /project-agents/{agentId}/sessions/{sessionId} {
allow read, write: if false;
}
// Collection group query rules (separate match)
match /{path=**}/sessions/{sessionId} {
allow read: if true;
}
Example: oneonone supports collectionGroup queries for sessions/messages
Reference: /Users/dylanr/work/2389/oneonone/firestore.rules:44-52
diff().affectedKeys() for any client-write validationmatch /{document=**} { allow read, write: if false; }Always develop locally with emulators. Never test directly in production.
firebase.json emulators section:
{
"emulators": {
"auth": { "port": 9099 },
"functions": { "port": 5001 },
"firestore": { "port": 8080 },
"hosting": { "port": 5000 },
"ui": { "enabled": true, "port": 4000 },
"singleProjectMode": true
}
}
Key settings:
singleProjectMode: true - Essential: Allows emulators to work togetherui.enabled: true - Essential: Access debug UI at http://127.0.0.1:4000All three projects use these exact settings
Example: oneonone's emulator configuration
Reference: /Users/dylanr/work/2389/oneonone/firebase.json:55-73
Pattern for Next.js/React apps:
// hosting/lib/firebase.ts
// ABOUTME: Firebase client-side configuration and initialization
// ABOUTME: Exports auth, firestore, and functions instances for use in components
import { initializeApp, getApps } from 'firebase/app';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { getFunctions, connectFunctionsEmulator } from 'firebase/functions';
const firebaseConfig = {
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
// ... other config
};
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export const auth = getAuth(app);
export const db = getFirestore(app);
export const functions = getFunctions(app, 'us-central1');
// Connect to emulators in development
if (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') {
const useEmulators = process.env.NEXT_PUBLIC_USE_EMULATORS === 'true';
if (useEmulators) {
console.log('🔧 Connecting to Firebase emulators...');
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });
connectFirestoreEmulator(db, '127.0.0.1', 8080);
connectFunctionsEmulator(functions, '127.0.0.1', 5001);
console.log('✅ Connected to emulators');
}
}
Environment variable (hosting/.env.local):
NEXT_PUBLIC_USE_EMULATORS=true
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
Important:
typeof window !== 'undefined' to avoid SSR issuesNEXT_PUBLIC_ prefix for client-side env vars in Next.jsdisableWarnings: true prevents auth emulator warning spamExample: oneonone's emulator detection pattern
Reference: /Users/dylanr/work/2389/oneonone/hosting/lib/firebase.ts:28-54
Export/Import Pattern:
# Data is automatically imported from this directory on startup
.firebase/emulator-data/
# Export data (preserves state)
firebase emulators:export ./backup
# Import data on startup
firebase emulators:start --import=./backup
# Fresh start (delete all data)
rm -rf .firebase/emulator-data
Important:
.firebase/ to .gitignoreDefault behavior: When you start emulators, they automatically import from .firebase/emulator-data/ if it exists.
# Start emulators (auto-imports data from .firebase/emulator-data)
firebase emulators:start
# Access emulator UI
open http://127.0.0.1:4000
# Make code changes (functions/hosting auto-reload)
# Test changes in browser or with curl
# Stop emulators (auto-exports data)
Ctrl+C
Emulator UI Features:
Auto-reload behavior:
All code examples use TypeScript:
tsconfig.json basics:
{
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "lib",
"rootDir": "..",
"sourceMap": true,
"moduleResolution": "node",
"isolatedModules": true
},
"include": ["src"],
"exclude": ["node_modules", "**/__tests__/**", "**/*.test.ts"]
}
Key settings:
strict: true - Enable all strict type checkingtarget: "es2017" - Matches Firebase Functions runtimeoutDir: "lib" - Standard Firebase Functions output directoryExample: bot-socialmedia's TypeScript configuration
Reference: /Users/dylanr/work/2389/bot-socialmedia-server/functions/tsconfig.json
Main config (vitest.config.ts):
// ABOUTME: Vitest configuration for Firebase Cloud Functions testing
// ABOUTME: Configures Node.js test environment with TypeScript support and coverage settings
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
globals: true,
setupFiles: './vitest.setup.ts',
include: ['**/__tests__/**/*.test.ts', '**/*.test.ts'],
exclude: ['**/node_modules/**', '**/lib/**', '**/__tests__/emulator/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
thresholds: {
branches: 50,
functions: 60,
lines: 60,
statements: 60,
},
},
clearMocks: true,
restoreMocks: true,
},
});
Emulator-specific config (vitest.emulator.config.ts):
// ABOUTME: Vitest configuration specifically for emulator tests
// ABOUTME: Used when running tests that require Firebase emulators
import { defineConfig, mergeConfig } from 'vitest/config';
import baseConfig from './vitest.config';
export default mergeConfig(
baseConfig,
defineConfig({
test: {
include: ['**/__tests__/emulator/**/*.test.ts'],
exclude: ['**/node_modules/**', '**/lib/**'],
},
})
);
Run commands:
# Unit tests (fast, no emulators)
npm run test
# Integration tests (with emulators)
npm run test:emulator
# Watch mode
npm run test -- --watch
# Coverage
npm run test -- --coverage
package.json scripts:
{
"scripts": {
"test": "vitest run",
"test:emulator": "vitest run --config vitest.emulator.config.ts",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Example: bot-socialmedia's vitest setup
Reference: /Users/dylanr/work/2389/bot-socialmedia-server/functions/vitest.config.ts
Configuration (biome.json):
{
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
}
}
Run commands:
# Check for issues
npm run lint
# Auto-fix issues
npm run lint:fix
# Format code
npm run format
package.json scripts:
{
"scripts": {
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write ."
}
}
Benefits:
Every TypeScript file starts with 2-line comment:
// ABOUTME: Brief description of what this file does
// ABOUTME: Second line with additional context
import { something } from 'somewhere';
// ... rest of file
Examples from production:
// ABOUTME: Main entry point for Firebase Functions - exports MCP endpoint with tool routing
// ABOUTME: Configures Express app with authentication, CORS, and health check
// ABOUTME: Post creation, reading, and management functions
// ABOUTME: Includes API endpoints and real-time triggers
// ABOUTME: Vitest configuration for Firebase Cloud Functions testing
// ABOUTME: Configures Node.js test environment with TypeScript support and coverage settings
Benefits:
grep "ABOUTME:" **/*.tsBoth TypeScript projects use this pattern
Unit Tests:
src/__tests__/ or next to source files*.test.tsIntegration Tests:
vitest.emulator.config.tssrc/__tests__/emulator/*.test.tsBoth types required for every feature
Example test structure:
functions/src/
├── __tests__/
│ ├── middleware/
│ │ └── apiKeyGuard.test.ts # Unit test
│ ├── tools/
│ │ └── requestSession.test.ts # Unit test
│ └── emulator/
│ └── mcp-workflow.test.ts # Integration test
Test naming convention:
describe() and it() blocks for organizationCoverage expectations:
Document these issues when encountered:
Emulator ports in use
lsof -i :5001Admin SDK vs Client SDK confusion
Rules testing mistakes
Cold start delays
Data persistence issues
.firebase/emulator-data/Node version compatibility
nodejs18 or nodejs20"engines": {"node": "20"}Environment variables
.env file (never commit).env.local with NEXT_PUBLIC_ prefixCORS in functions
app.use(cors({ origin: true }))cors: true in onRequest optionsIndex requirements
firestore.indexes.jsonDeployment order
Emulators: Free, no charges during local development
Production Costs:
Why Server-Write-Only Can Be Cost-Effective:
Monitor costs: Firebase Console → Usage and Billing
This Firebase development skill system provides:
Next Steps:
Sub-Skills: