Add new Cloud Functions, Firestore collections, or API endpoints to Firebase project following TDD. Guides through test-first development, architecture patterns, security rules, and emulator verification.
/plugin marketplace add 2389-research/claude-plugins/plugin install firebase-development@2389-research-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This sub-skill guides you through adding new features to an existing Firebase project using Test-Driven Development (TDD). It handles:
The workflow uses TodoWrite to track 12 steps from feature identification to emulator verification.
Use this sub-skill when:
Do not use for:
This sub-skill integrates with superpowers skills for quality enforcement:
TDD Integration:
Verification Integration:
Pattern Integration:
All patterns are documented in the main @firebase-development skill. This sub-skill helps you implement those patterns for new features.
Key Patterns Referenced:
This sub-skill creates a TodoWrite checklist with 12 steps. Follow the checklist to systematically add your feature using TDD.
Actions:
Analyze the user's request to determine feature type:
Feature Types:
Use AskUserQuestion if unclear:
Question: "What type of Firebase feature are you adding?"
Header: "Feature Type"
Options:
- "HTTP Endpoint" (API route for Express app or standalone)
- "Firestore Trigger" (Runs when documents change)
- "Scheduled Function" (Cron job or periodic task)
- "Callable Function" (Direct client SDK calls)
Store the feature type for use in subsequent steps.
Actions:
Examine the existing project to understand patterns:
# Check functions architecture
ls -la functions/src/
# Look for existing patterns
grep -r "onRequest" functions/src/
grep -r "export" functions/src/index.ts
# Check if Express is used
grep "express" functions/package.json
# Check authentication patterns
ls -la functions/src/middleware/
grep -r "request.auth" functions/src/
grep -r "x-api-key" functions/src/
Determine:
Reference main skill: @firebase-development ’ Cloud Functions Architecture, Authentication
Actions:
Following TDD: Write the test FIRST, watch it fail.
Create test file based on architecture:
For Express API (functions/src/tests/tools/yourFeature.test.ts):
// ABOUTME: Unit tests for [feature name] functionality
// ABOUTME: Tests [what the feature does] with various input scenarios
import { describe, it, expect, vi } from 'vitest';
import { handleYourFeature } from '../../tools/yourFeature';
describe('handleYourFeature', () => {
it('should return success when given valid input', async () => {
const userId = 'test-user-123';
const params = {
// Your expected parameters
name: 'test',
value: 42
};
const result = await handleYourFeature(userId, params);
expect(result.success).toBe(true);
expect(result.message).toBeDefined();
expect(result.data).toBeDefined();
});
it('should return error when given invalid input', async () => {
const userId = 'test-user-123';
const params = {
// Invalid parameters
name: '', // Empty name should fail
};
const result = await handleYourFeature(userId, params);
expect(result.success).toBe(false);
expect(result.message).toContain('Invalid');
});
it('should validate authentication', async () => {
const userId = ''; // Empty userId should fail
const params = { name: 'test', value: 42 };
const result = await handleYourFeature(userId, params);
expect(result.success).toBe(false);
expect(result.message).toContain('authentication');
});
});
For Domain-Grouped (functions/src/tests/domainName.test.ts):
// ABOUTME: Unit tests for [domain] functionality
// ABOUTME: Tests all functions in [domain] file
import { describe, it, expect } from 'vitest';
import { yourFunction } from '../domainName';
import * as admin from 'firebase-admin';
describe('Domain: yourFunction', () => {
it('should handle valid requests', async () => {
// Mock request/response
const mockReq = {
auth: { uid: 'test-user' },
body: { data: 'test' }
};
const mockRes = {
json: vi.fn(),
status: vi.fn().mockReturnThis(),
};
await yourFunction(mockReq as any, mockRes as any);
expect(mockRes.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
message: expect.any(String),
})
);
});
});
For Firestore Trigger:
// ABOUTME: Tests for [collection] Firestore triggers
// ABOUTME: Verifies trigger behavior on document create/update/delete
import { describe, it, expect, beforeAll } from 'vitest';
import * as admin from 'firebase-admin';
describe('Firestore Trigger: onDocumentCreated', () => {
beforeAll(() => {
if (admin.apps.length === 0) {
admin.initializeApp({ projectId: 'test-project' });
}
});
it('should process new document correctly', async () => {
// This will fail until implementation exists
const testDoc = {
id: 'test-123',
name: 'Test Document',
createdAt: new Date(),
};
// Test your trigger logic
expect(true).toBe(false); // Intentional failure - implement this!
});
});
Run test to confirm it fails:
cd functions
npm run test
Expected: Test fails because implementation doesn't exist yet. This confirms TDD process.
Reference: superpowers:test-driven-development
Actions:
Create the function file based on architecture pattern:
For Express API (functions/src/tools/yourFeature.ts):
// ABOUTME: Implements [feature name] functionality for [purpose]
// ABOUTME: Handles [what it does] and returns {success, message, data?} response
import * as admin from 'firebase-admin';
/**
* Response type for handler functions
*/
export interface HandlerResponse {
success: boolean;
message: string;
data?: any;
}
/**
* Parameters for this feature
*/
export interface YourFeatureParams {
name: string;
value: number;
// Add your parameters
}
/**
* Handles [feature name]
*
* @param userId - Authenticated user ID
* @param params - Feature parameters
* @returns Promise with {success, message, data?}
*/
export async function handleYourFeature(
userId: string,
params: YourFeatureParams
): Promise<HandlerResponse> {
try {
// Validate authentication
if (!userId) {
return {
success: false,
message: 'Authentication required',
};
}
// Validate input
if (!params.name || params.name.trim() === '') {
return {
success: false,
message: 'Invalid input: name is required',
};
}
// Get Firestore instance
const db = admin.firestore();
// Implement your feature logic here
// Example: Create a document
const docRef = db.collection('yourCollection').doc();
await docRef.set({
userId,
name: params.name,
value: params.value,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
});
return {
success: true,
message: 'Feature executed successfully',
data: {
id: docRef.id,
name: params.name,
value: params.value,
},
};
} catch (error) {
console.error('Error in handleYourFeature:', error);
return {
success: false,
message: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
};
}
}
For Domain-Grouped (functions/src/yourDomain.ts):
// ABOUTME: [Domain name] functions for [purpose]
// ABOUTME: Handles [list main responsibilities]
import { onRequest } from 'firebase-functions/v2/https';
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
import * as admin from 'firebase-admin';
export const yourFunction = onRequest(async (req, res) => {
try {
// Validate authentication
if (!req.auth) {
res.status(401).json({
success: false,
message: 'Authentication required',
});
return;
}
// Validate input
const { data } = req.body;
if (!data) {
res.status(400).json({
success: false,
message: 'Invalid input',
});
return;
}
// Implement logic
const result = {
success: true,
message: 'Operation successful',
data: { /* your response data */ },
};
res.status(200).json(result);
} catch (error) {
console.error('Error in yourFunction:', error);
res.status(500).json({
success: false,
message: 'Internal server error',
});
}
});
// If adding Firestore trigger
export const onYourTrigger = onDocumentCreated(
'yourCollection/{docId}',
async (event) => {
const snapshot = event.data;
if (!snapshot) {
console.log('No data associated with the event');
return;
}
const data = snapshot.data();
console.log('Processing:', data);
// Implement trigger logic
}
);
For Individual Files (functions/functions/yourFeature.js):
const { onRequest } = require('firebase-functions/v2/https');
const admin = require('firebase-admin');
exports.yourFeature = onRequest(async (req, res) => {
try {
// Implementation
res.json({
success: true,
message: 'Feature executed successfully',
});
} catch (error) {
console.error('Error:', error);
res.status(500).json({
success: false,
message: 'Error occurred',
});
}
});
Reference main skill: @firebase-development ’ Cloud Functions Architecture
Actions:
Add rules for any new Firestore collections:
For Server-Write-Only Pattern:
// Add to firestore.rules
match /yourCollection/{docId} {
allow read: if request.auth != null; // Authenticated users can read
allow write: if false; // Only Cloud Functions can write
}
// If documents are user-specific
match /users/{userId}/yourCollection/{docId} {
allow read: if request.auth != null && request.auth.uid == userId;
allow write: if false; // Only Cloud Functions can write
}
For Client-Write Pattern:
// Add to firestore.rules
match /yourCollection/{docId} {
// Allow authenticated users to create their own documents
allow create: if request.auth != null &&
request.resource.data.userId == request.auth.uid &&
request.resource.data.createdAt == request.time;
// Allow users to read their own documents
allow read: if request.auth != null &&
resource.data.userId == request.auth.uid;
// Allow users to update only specific safe fields
allow update: if request.auth != null &&
resource.data.userId == request.auth.uid &&
request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['name', 'value', 'updatedAt']);
// Allow users to delete their own documents
allow delete: if request.auth != null &&
resource.data.userId == request.auth.uid;
}
For Admin-Only Collections:
// Add helper function if not exists
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
// Add collection rules
match /adminCollection/{docId} {
allow read: if request.auth != null && isAdmin();
allow write: if request.auth != null && isAdmin();
}
Test rules in Emulator UI:
Reference main skill: @firebase-development ’ Firestore Rules Patterns
Actions:
If your feature uses complex queries, add indexes to firestore.indexes.json:
Check if indexes are needed:
where clausesorderBy + whereExample firestore.indexes.json:
{
"indexes": [
{
"collectionGroup": "yourCollection",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "yourCollection",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "status",
"order": "ASCENDING"
},
{
"fieldPath": "updatedAt",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": []
}
If no complex queries: Skip this step, single-field indexes are automatic.
Note: When you run a query that needs an index, Firebase will provide a URL to create it automatically.
Actions:
Add authentication based on project pattern:
For API Keys (Express Middleware):
Update functions/src/index.ts to use middleware:
import { apiKeyGuard } from './middleware/apiKeyGuard';
// Apply to specific route
app.post('/your-endpoint', apiKeyGuard, async (req, res) => {
const userId = req.userId!; // Set by middleware
// Your handler code
});
For Firebase Auth (Express):
// Add auth middleware
async function authGuard(req: Request, res: Response, next: NextFunction) {
if (!req.auth || !req.auth.uid) {
res.status(401).json({
success: false,
message: 'Authentication required',
});
return;
}
next();
}
// Apply to route
app.post('/your-endpoint', authGuard, async (req, res) => {
const userId = req.auth!.uid;
// Your handler code
});
For Callable Functions:
import { onCall } from 'firebase-functions/v2/https';
export const yourCallable = onCall(async (request) => {
// Auth automatically provided
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Authentication required');
}
const userId = request.auth.uid;
// Your logic
});
For Domain-Grouped/Individual:
// Check auth in each function
export const yourFunction = onRequest(async (req, res) => {
if (!req.auth) {
res.status(401).json({ success: false, message: 'Authentication required' });
return;
}
const userId = req.auth.uid;
// Your logic
});
Reference main skill: @firebase-development ’ Authentication
Actions:
Ensure all handlers use consistent response pattern:
Response Pattern (Universal):
interface HandlerResponse {
success: boolean; // Required: true if successful, false if error
message: string; // Required: human-readable message
data?: any; // Optional: response payload when successful
}
Success Response:
return {
success: true,
message: 'Operation completed successfully',
data: {
id: docId,
// ... your data
},
};
Error Response:
return {
success: false,
message: 'Error: Invalid input provided',
// No data field on errors
};
For Express Routes:
app.post('/endpoint', apiKeyGuard, async (req, res) => {
const result = await handleYourFeature(req.userId!, req.body);
const statusCode = result.success ? 200 : 400;
res.status(statusCode).json(result);
});
Validation Checks (Defense in Depth):
// 1. Validate authentication
if (!userId) {
return { success: false, message: 'Authentication required' };
}
// 2. Validate required parameters
if (!params.name || !params.value) {
return { success: false, message: 'Missing required parameters' };
}
// 3. Validate business logic
if (params.value < 0) {
return { success: false, message: 'Value must be positive' };
}
// 4. Execute with error handling
try {
// Your logic
return { success: true, message: 'Success', data: {...} };
} catch (error) {
return { success: false, message: `Error: ${error.message}` };
}
Reference: superpowers:defense-in-depth
Actions:
Export your new function based on architecture:
For Express API (functions/src/index.ts):
import { handleYourFeature } from './tools/yourFeature';
app.post('/mcp', apiKeyGuard, async (req, res) => {
const { tool, params } = req.body;
const userId = req.userId!;
let result;
switch (tool) {
case 'your_feature': // Add your case
result = await handleYourFeature(userId, params);
break;
// ... existing cases
default:
res.status(400).json({ success: false, error: 'Unknown tool' });
return;
}
res.status(200).json(result);
});
// Or add as separate route
app.post('/your-endpoint', apiKeyGuard, async (req, res) => {
const result = await handleYourFeature(req.userId!, req.body);
res.status(result.success ? 200 : 400).json(result);
});
For Domain-Grouped (functions/src/index.ts):
export * from './yourDomain';
For Individual Files (functions/index.js):
const { yourFeature } = require("./functions/yourFeature");
exports.yourFeature = yourFeature;
Verify export:
cd functions
npm run build
# Check compiled output
ls -la lib/
Expected: Function appears in compiled output
Actions:
Now that implementation exists, verify tests pass:
cd functions
npm run test
Expected: All tests pass
If tests fail:
Add additional test cases:
// Test edge cases
it('should handle empty input gracefully', async () => {
const result = await handleYourFeature('user-123', {} as any);
expect(result.success).toBe(false);
});
// Test error handling
it('should handle Firestore errors', async () => {
// Mock Firestore to throw error
vi.spyOn(admin.firestore(), 'collection').mockImplementation(() => {
throw new Error('Firestore error');
});
const result = await handleYourFeature('user-123', { name: 'test', value: 42 });
expect(result.success).toBe(false);
expect(result.message).toContain('Error');
});
// Test success path with data
it('should return data on success', async () => {
const result = await handleYourFeature('user-123', { name: 'test', value: 42 });
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.id).toBeDefined();
});
Run linting:
npm run lint:fix
Reference: superpowers:test-driven-development (GREEN phase)
Actions:
Create integration test that runs against emulators:
Create functions/src/tests/emulator/yourFeature.test.ts:
// ABOUTME: Integration test for [feature name] using Firebase emulators
// ABOUTME: Tests complete workflow from HTTP request to Firestore persistence
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import * as admin from 'firebase-admin';
import fetch from 'node-fetch';
describe('Integration: yourFeature', () => {
let testUserId: string;
let testApiKey: string;
beforeAll(async () => {
// Initialize admin SDK for emulator
if (admin.apps.length === 0) {
admin.initializeApp({
projectId: 'demo-test-project',
});
}
// Connect to Firestore emulator
const db = admin.firestore();
db.settings({
host: '127.0.0.1:8080',
ssl: false,
});
// Create test user and API key
testUserId = 'integration-test-user';
testApiKey = 'myproj_integration_test_key';
await db
.collection('users')
.doc(testUserId)
.collection('apiKeys')
.doc(testApiKey)
.set({
keyId: testApiKey,
userId: testUserId,
active: true,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
});
});
afterAll(async () => {
// Cleanup test data
const db = admin.firestore();
await db
.collection('users')
.doc(testUserId)
.collection('apiKeys')
.doc(testApiKey)
.delete();
});
it('should create document via HTTP endpoint', async () => {
const response = await fetch(
'http://127.0.0.1:5001/demo-test-project/us-central1/api/your-endpoint',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': testApiKey,
},
body: JSON.stringify({
name: 'integration-test',
value: 42,
}),
}
);
const result = await response.json();
expect(response.status).toBe(200);
expect(result.success).toBe(true);
expect(result.data).toBeDefined();
expect(result.data.id).toBeDefined();
// Verify document was created in Firestore
const db = admin.firestore();
const doc = await db.collection('yourCollection').doc(result.data.id).get();
expect(doc.exists).toBe(true);
expect(doc.data()?.name).toBe('integration-test');
expect(doc.data()?.value).toBe(42);
});
it('should reject invalid API key', async () => {
const response = await fetch(
'http://127.0.0.1:5001/demo-test-project/us-central1/api/your-endpoint',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': 'invalid-key',
},
body: JSON.stringify({ name: 'test', value: 42 }),
}
);
expect(response.status).toBe(401);
});
});
Run integration tests (requires emulators running):
# Terminal 1: Start emulators
firebase emulators:start
# Terminal 2: Run integration tests
cd functions
npm run test:emulator
Expected: Integration tests pass
Actions:
Start emulators and test your feature:
# Start emulators
firebase emulators:start
Open Emulator UI:
open http://127.0.0.1:4000
Test your endpoint with curl:
For Express API:
# Replace with your actual endpoint and API key
curl -X POST http://127.0.0.1:5001/[project-id]/us-central1/api/your-endpoint \
-H "Content-Type: application/json" \
-H "x-api-key: your_test_api_key" \
-d '{"name":"test","value":42}'
For Callable Functions:
// In your client app
const yourCallable = httpsCallable(functions, 'yourCallable');
const result = await yourCallable({ name: 'test', value: 42 });
console.log(result.data);
Verify in Emulator UI:
Test Firestore Rules:
Verify Success Criteria:
Reference: superpowers:verification-before-completion
All handlers MUST use this response pattern:
interface HandlerResponse {
success: boolean;
message: string;
data?: any;
}
Examples:
// Success
{
success: true,
message: "Document created successfully",
data: { id: "abc123", name: "example" }
}
// Error
{
success: false,
message: "Invalid input: name is required"
}
// Success without data
{
success: true,
message: "Document deleted successfully"
}
This sub-skill responds to:
tools/ directoryfunctions/ directory/users/{userId}/yourCollection/{docId}
- userId: string (redundant but useful)
- createdAt: timestamp
- updatedAt: timestamp
- active: boolean
/yourCollection/{docId}
- userId: string (owner)
- createdAt: timestamp
- updatedAt: timestamp
- status: string
/teams/{teamId}/members/{userId}
- role: string
- joinedAt: timestamp
// Middleware validates and sets req.userId
app.post('/endpoint', apiKeyGuard, async (req, res) => {
const userId = req.userId!;
// ...
});
// Check req.auth.uid
export const yourFunction = onRequest(async (req, res) => {
if (!req.auth) {
res.status(401).json({ success: false, message: 'Auth required' });
return;
}
const userId = req.auth.uid;
// ...
});
export const yourCallable = onCall(async (request) => {
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Auth required');
}
const userId = request.auth.uid;
// ...
});
After completing all TodoWrite steps, verify:
After feature is complete and verified:
git add .
git commit -m "feat: add [feature name]
Implements [what the feature does] with TDD.
- Write failing tests first
- Implement handler with {success, message, data?} pattern
- Add Firestore rules for [collection]
- Verify with emulator testing
> Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>"
Consider code review: Use @superpowers/requesting-code-review
Deploy when ready:
# Deploy just functions
firebase deploy --only functions
# Deploy functions + rules
firebase deploy --only functions,firestore:rules
# Deploy everything
firebase deploy
All patterns used in this workflow are documented in the main skill:
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.