From atum-stack-backend
Firebase implementation pattern library — Firestore data modeling (subcollections, denormalization, fan-out, counter sharding for high-write counters), Firestore Security Rules DSL with rules emulator testing, Cloud Functions Gen 2 idempotency patterns + cold start mitigation + secret management via Cloud Secret Manager, Auth flows (Email/Google/Apple/Phone with custom claims for RBAC, MFA TOTP, Anonymous auth + linking), Cloud Storage rules with image transforms, FCM push notifications (token management, topics, conditional sends), App Check enforcement (App Attest / Play Integrity / reCAPTCHA), Remote Config feature flags with conditional rollout, and Vertex AI in Firebase for direct Gemini calls from clients. Use when scaffolding a Firebase project, designing Firestore schemas, writing security rules, or hardening an existing Firebase backend. Mentions the official `firebase` MCP server (npx firebase-tools@latest mcp) declared in this plugin's .mcp.json — Claude Code can use it to introspect and deploy live projects. Differentiates from generic NoSQL patterns by Firebase-specific cost models (per-read pricing) and security-rules-first architecture.
npx claudepluginhub arnwaldn/atum-plugins-collection --plugin atum-stack-backendThis skill uses the workspace's default tool permissions.
Ce skill couvre les patterns concrets pour construire un backend Firebase production-grade. Il complète l'agent `firebase-expert` en donnant des recettes prêtes à coller.
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.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides agent creation for Claude Code plugins with file templates, frontmatter specs (name, description, model), triggering examples, system prompts, and best practices.
Ce skill couvre les patterns concrets pour construire un backend Firebase production-grade. Il complète l'agent firebase-expert en donnant des recettes prêtes à coller.
MCP server disponible : ce plugin déclare firebase dans .mcp.json. Claude Code peut invoquer firebase-tools directement pour lister les projets, déployer, lire les logs.
Subcollections quand :
users/{userId}/notifications/{notifId}Root collections + foreign key quand :
projects avec ownerIdUn document Firestore = 1 write/sec max. Pour un compteur de likes/views/votes :
// 10 shards aléatoires
const NUM_SHARDS = 10
async function incrementLikeCount(postId: string) {
const shardId = Math.floor(Math.random() * NUM_SHARDS)
const shardRef = db.doc(`posts/${postId}/likes_shards/${shardId}`)
await shardRef.set({ count: FieldValue.increment(1) }, { merge: true })
}
async function getLikeCount(postId: string) {
const shards = await db.collection(`posts/${postId}/likes_shards`).get()
return shards.docs.reduce((sum, doc) => sum + (doc.data().count ?? 0), 0)
}
Stocker le userName et userAvatar dans chaque post au lieu de joindre, parce que Firestore n'a PAS de jointures et chaque lookup = facturation.
Synchronisation via Cloud Function users.onUpdate qui propage le changement aux posts.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isAppCheckValid() {
return request.app != null && request.app.attested == true;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function tenantId() {
return request.auth.token.tenant_id;
}
function hasRole(role) {
return request.auth.token.role == role;
}
// Empêche tout par défaut
match /{document=**} {
allow read, write: if false;
}
match /tenants/{tid}/projects/{pid} {
allow read: if isSignedIn() && isAppCheckValid() && tid == tenantId();
allow create: if isSignedIn() && isAppCheckValid() && tid == tenantId()
&& request.resource.data.ownerId == request.auth.uid
&& request.resource.data.keys().hasOnly(['name', 'ownerId', 'createdAt']);
allow update: if isSignedIn() && isAppCheckValid() && tid == tenantId()
&& (isOwner(resource.data.ownerId) || hasRole('admin'));
allow delete: if isSignedIn() && isAppCheckValid() && hasRole('admin');
}
}
}
// firestore.rules.test.ts
import { initializeTestEnvironment, assertSucceeds, assertFails } from '@firebase/rules-unit-testing'
const env = await initializeTestEnvironment({
projectId: 'test-project',
firestore: { rules: readFileSync('firestore.rules', 'utf8') },
})
const alice = env.authenticatedContext('alice', { tenant_id: 'tenantA', role: 'user' })
const bob = env.authenticatedContext('bob', { tenant_id: 'tenantB', role: 'user' })
await assertSucceeds(alice.firestore().doc('tenants/tenantA/projects/p1').get())
await assertFails(bob.firestore().doc('tenants/tenantA/projects/p1').get())
firebase emulators:exec --only firestore "vitest run firestore.rules.test.ts"
import { onDocumentCreated } from 'firebase-functions/v2/firestore'
import { defineSecret } from 'firebase-functions/params'
import { initializeApp } from 'firebase-admin/app'
import { getFirestore, FieldValue } from 'firebase-admin/firestore'
initializeApp()
const db = getFirestore()
const sendgridKey = defineSecret('SENDGRID_API_KEY')
export const onNewOrder = onDocumentCreated(
{
document: 'orders/{orderId}',
secrets: [sendgridKey],
region: 'europe-west1',
minInstances: 1,
},
async (event) => {
const order = event.data?.data()
if (!order) return
// IDEMPOTENCY: utilise event.id comme clé unique
const processedRef = db.doc(`processed_events/${event.id}`)
const processed = await processedRef.get()
if (processed.exists) {
console.log('Already processed', event.id)
return
}
try {
// ... logique métier ...
await sendOrderConfirmation(order, sendgridKey.value())
// Marque comme traité APRÈS le succès
await processedRef.set({ processedAt: FieldValue.serverTimestamp() })
} catch (err) {
console.error('Failed to process order', err)
throw err // Firebase retry
}
}
)
import { onCall, HttpsError } from 'firebase-functions/v2/https'
export const refundOrder = onCall(
{ region: 'europe-west1', enforceAppCheck: true },
async (request) => {
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Sign in required')
}
if (request.auth.token.role !== 'admin') {
throw new HttpsError('permission-denied', 'Admin only')
}
const { orderId } = request.data
if (!orderId || typeof orderId !== 'string') {
throw new HttpsError('invalid-argument', 'orderId required')
}
// ... refund logic ...
return { success: true }
}
)
import { getAuth } from 'firebase-admin/auth'
// Côté admin (Cloud Function ou backend privilégié)
await getAuth().setCustomUserClaims(userId, {
tenant_id: 'tenantA',
role: 'admin',
})
// Le client doit refresh son token pour voir les nouveaux claims :
// firebase.auth().currentUser?.getIdToken(true)
// Client web
import { getAuth, signInAnonymously, linkWithCredential, EmailAuthProvider } from 'firebase/auth'
const auth = getAuth()
// 1. User commence en anonymous
await signInAnonymously(auth)
// 2. Plus tard, il s'inscrit
const credential = EmailAuthProvider.credential('user@example.com', 'password')
await linkWithCredential(auth.currentUser!, credential)
// → Garde le même UID, conserve toutes les données Firestore liées
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /users/{userId}/avatars/{filename} {
allow read: if true; // Avatars publics
allow write: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 5 * 1024 * 1024 // 5 MB max
&& request.resource.contentType.matches('image/.*');
}
}
}
import { initializeAppCheck, ReCaptchaEnterpriseProvider } from 'firebase/app-check'
const appCheck = initializeAppCheck(app, {
provider: new ReCaptchaEnterpriseProvider('SITE_KEY'),
isTokenAutoRefreshEnabled: true,
})
request.app.attested == true dans les rulesenforceAppCheck: true dans les optionsActiver en mode "Unenforced" dans Firebase Console pour 2-4 semaines, surveiller la distribution des requêtes attestées vs non-attestées dans Metrics, puis switcher en "Enforced" quand >99% des requêtes légitimes sont attestées.
// Côté client mobile (iOS Swift, Android Kotlin, web)
const token = await getToken(messaging, { vapidKey: VAPID_KEY })
await db.doc(`users/${userId}/devices/${deviceId}`).set({
token,
platform: 'ios',
updatedAt: FieldValue.serverTimestamp(),
})
import { getMessaging } from 'firebase-admin/messaging'
const tokens = (await db.collection(`users/${userId}/devices`).get())
.docs.map(d => d.data().token)
await getMessaging().sendEachForMulticast({
tokens,
notification: { title: 'Nouvelle commande', body: '#42 reçue' },
data: { orderId: '42' },
})
import { getVertexAI, getGenerativeModel } from 'firebase/vertexai'
const vertexAI = getVertexAI(firebaseApp)
const model = getGenerativeModel(vertexAI, { model: 'gemini-1.5-flash' })
const result = await model.generateContent('Résume ce texte en 3 lignes : ...')
console.log(result.response.text())
App Check est obligatoire pour empêcher l'abus.
# Export programmé via gcloud
gcloud firestore export gs://my-backup-bucket/$(date +%F)
# Restore (vers un nouveau projet ou un staging)
gcloud firestore import gs://my-backup-bucket/2026-04-08
À automatiser avec une Cloud Scheduler + Cloud Function ou un job pg_cron-style.
allow read, write: if true — exfiltration de toute la baseonSnapshot non-détaché — fuite mémoire + facturationkeys().hasOnly([...]) dans les rules — injection de champs malveillantsnosql-specialistflutter-dart-expert, expo-expertfirebase-expert (j'ai aussi supabase-architect)auth-providers (atum-stack-web)