AgentSkillsCN

Firebase

Firebase

SKILL.md

🔥 Agent Skill: Firebase

Rôle

Guider l'agent dans l'utilisation correcte de Firebase (Firestore, Storage, Auth) pour les modules temps réel du projet RadioManager SaaS : Chat, Tasks et Quotes.

Quand utiliser ce skill

Déclencheurs automatiques

  • L'agent travaille sur les modules Chat, Tasks ou Quotes
  • Création ou modification d'opérations Firestore
  • Upload/download de fichiers (Firebase Storage)
  • Gestion de listeners temps réel (onSnapshot)
  • Configuration ou règles de sécurité Firebase
  • Debug d'erreurs Firebase

Contexte d'utilisation

  • Systématique : Toute opération sur les données temps réel
  • Lors de la création de nouveaux stores Zustand utilisant Firebase
  • Pour les opérations CRUD sur Firestore
  • Quand on manipule des fichiers audio/images

📋 Architecture Firebase du Projet

Configuration

typescript
// src/api/firebase/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
import { getAuth } from 'firebase/auth';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
  appId: import.meta.env.VITE_FIREBASE_APP_ID,
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);

Collections Firestore

CollectionModuleDescriptionDocuments
roomsChatSalons de discussionChatRoom
rooms/{id}/messagesChatMessages d'un salonMessage
tasksTasksTâches KanbanTask
quotesQuotesCitationsQuote
versionsSystemHistorique versionsVersion
userPreferencesSystemPréférences utilisateurUserPrefs

🔧 Ce que l'agent DOIT faire

1. Imports Firestore Corrects

typescript
// ✅ BON : Imports spécifiques (tree-shaking)
import {
  collection,
  doc,
  addDoc,
  setDoc,
  updateDoc,
  deleteDoc,
  getDoc,
  getDocs,
  query,
  where,
  orderBy,
  onSnapshot,
  serverTimestamp,
  Timestamp,
  writeBatch,
} from 'firebase/firestore';
import { db } from '../api/firebase/firebase';

// ❌ MAUVAIS : Import global
import firebase from 'firebase/app';

2. Pattern CRUD Firestore

Créer un document

typescript
// ✅ BON : Avec addDoc (ID auto-généré)
const createQuote = async (data: CreateQuoteData): Promise<Quote> => {
  try {
    const docRef = await addDoc(collection(db, 'quotes'), {
      ...data,
      createdAt: serverTimestamp(),
      updatedAt: serverTimestamp(),
    });
    
    return { id: docRef.id, ...data };
  } catch (error) {
    console.error('Erreur création:', error);
    throw new Error('Impossible de créer la citation');
  }
};

// ✅ BON : Avec setDoc (ID personnalisé)
const createRoom = async (room: ChatRoom): Promise<void> => {
  const roomRef = doc(db, 'rooms', room.id);
  await setDoc(roomRef, {
    ...room,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });
};

Lire des documents

typescript
// ✅ BON : Lecture unique
const getQuote = async (id: string): Promise<Quote | null> => {
  const docRef = doc(db, 'quotes', id);
  const docSnap = await getDoc(docRef);
  
  if (!docSnap.exists()) return null;
  
  return { id: docSnap.id, ...docSnap.data() } as Quote;
};

// ✅ BON : Lecture avec filtres
const getQuotesByStatus = async (status: QuoteStatus): Promise<Quote[]> => {
  const q = query(
    collection(db, 'quotes'),
    where('status', '==', status),
    orderBy('createdAt', 'desc')
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Quote));
};

Mettre à jour un document

typescript
// ✅ BON : Update partiel avec updateDoc
const updateQuote = async (id: string, updates: Partial<Quote>): Promise<void> => {
  const docRef = doc(db, 'quotes', id);
  await updateDoc(docRef, {
    ...updates,
    updatedAt: serverTimestamp(),
  });
};

// ✅ BON : Remplacement complet avec setDoc + merge
const replaceQuote = async (id: string, data: Quote): Promise<void> => {
  const docRef = doc(db, 'quotes', id);
  await setDoc(docRef, {
    ...data,
    updatedAt: serverTimestamp(),
  }, { merge: true });
};

Supprimer un document

typescript
// ✅ BON : Suppression simple
const deleteQuote = async (id: string): Promise<void> => {
  const docRef = doc(db, 'quotes', id);
  await deleteDoc(docRef);
};

// ✅ BON : Soft delete (recommandé)
const archiveQuote = async (id: string): Promise<void> => {
  const docRef = doc(db, 'quotes', id);
  await updateDoc(docRef, {
    status: 'archived',
    archivedAt: serverTimestamp(),
  });
};

3. Listeners Temps Réel (onSnapshot)

typescript
// ✅ BON : Pattern avec unsubscribe dans Zustand store
export const useTaskStore = create<TaskState>()((set, get) => ({
  tasks: [],
  unsubscribe: null as (() => void) | null,

  subscribeToTasks: () => {
    // Désabonner l'ancien listener s'il existe
    const currentUnsub = get().unsubscribe;
    if (currentUnsub) currentUnsub();

    const q = query(
      collection(db, 'tasks'),
      orderBy('createdAt', 'desc')
    );

    const unsubscribe = onSnapshot(q, 
      (snapshot) => {
        const tasks = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        } as Task));
        set({ tasks, isLoading: false });
      },
      (error) => {
        console.error('Erreur subscription:', error);
        set({ error: error.message, isLoading: false });
      }
    );

    set({ unsubscribe });
  },

  // ✅ IMPORTANT : Nettoyer à la déconnexion
  cleanup: () => {
    const unsub = get().unsubscribe;
    if (unsub) unsub();
    set({ unsubscribe: null });
  },
}));

4. Sous-collections (Messages dans Rooms)

typescript
// ✅ BON : Accès sous-collection
const getMessages = async (roomId: string): Promise<Message[]> => {
  const messagesRef = collection(db, `rooms/${roomId}/messages`);
  const q = query(messagesRef, orderBy('timestamp', 'asc'));
  const snapshot = await getDocs(q);
  
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Message));
};

// ✅ BON : Ajouter à une sous-collection
const addMessage = async (roomId: string, message: Message): Promise<void> => {
  const messagesRef = collection(db, `rooms/${roomId}/messages`);
  await addDoc(messagesRef, {
    ...message,
    timestamp: serverTimestamp(),
  });
};

// ✅ BON : Listener sur sous-collection
const subscribeToMessages = (roomId: string, callback: (messages: Message[]) => void) => {
  const messagesRef = collection(db, `rooms/${roomId}/messages`);
  const q = query(messagesRef, orderBy('timestamp', 'asc'));
  
  return onSnapshot(q, (snapshot) => {
    const messages = snapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data()
    } as Message));
    callback(messages);
  });
};

5. Opérations Batch (Transactions)

typescript
// ✅ BON : Batch pour opérations multiples
import { writeBatch } from 'firebase/firestore';

const deleteRoomWithMessages = async (roomId: string): Promise<void> => {
  const batch = writeBatch(db);
  
  // 1. Récupérer tous les messages
  const messagesRef = collection(db, `rooms/${roomId}/messages`);
  const messagesSnapshot = await getDocs(messagesRef);
  
  // 2. Ajouter chaque message au batch de suppression
  messagesSnapshot.docs.forEach((doc) => {
    batch.delete(doc.ref);
  });
  
  // 3. Ajouter la room au batch
  batch.delete(doc(db, 'rooms', roomId));
  
  // 4. Exécuter tout en une fois
  await batch.commit();
};

// ✅ BON : Transaction pour lecture-écriture atomique
import { runTransaction } from 'firebase/firestore';

const incrementUnreadCount = async (roomId: string, userId: string): Promise<void> => {
  const roomRef = doc(db, 'rooms', roomId);
  
  await runTransaction(db, async (transaction) => {
    const roomDoc = await transaction.get(roomRef);
    if (!roomDoc.exists()) throw new Error('Room not found');
    
    const currentCount = roomDoc.data().unreadCount?.[userId] || 0;
    transaction.update(roomRef, {
      [`unreadCount.${userId}`]: currentCount + 1,
    });
  });
};

6. Firebase Storage (Fichiers)

typescript
import { ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage';
import { storage } from '../api/firebase/firebase';

// ✅ BON : Upload de fichier
const uploadAudioFile = async (file: File, quoteId: string): Promise<string> => {
  // Validation du fichier
  const maxSize = 10 * 1024 * 1024; // 10MB
  const allowedTypes = ['audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/mp3'];
  
  if (file.size > maxSize) {
    throw new Error('Fichier trop volumineux (max 10MB)');
  }
  if (!allowedTypes.includes(file.type)) {
    throw new Error('Type de fichier non autorisé');
  }
  
  // Upload
  const fileName = `quotes/${quoteId}/${Date.now()}_${file.name}`;
  const storageRef = ref(storage, fileName);
  
  const snapshot = await uploadBytes(storageRef, file);
  const downloadURL = await getDownloadURL(snapshot.ref);
  
  return downloadURL;
};

// ✅ BON : Supprimer un fichier
const deleteAudioFile = async (fileUrl: string): Promise<void> => {
  try {
    const storageRef = ref(storage, fileUrl);
    await deleteObject(storageRef);
  } catch (error) {
    // Ignorer si fichier n'existe pas
    console.warn('Fichier non trouvé:', error);
  }
};

7. Conversion des Timestamps

typescript
import { Timestamp } from 'firebase/firestore';

// ✅ BON : Helper de conversion
const timestampToISO = (timestamp: any): string => {
  if (!timestamp) return new Date().toISOString();
  
  // Timestamp Firestore
  if (timestamp instanceof Timestamp) {
    return timestamp.toDate().toISOString();
  }
  
  // Objet avec méthode toDate (snapshot)
  if (timestamp?.toDate) {
    return timestamp.toDate().toISOString();
  }
  
  // Déjà une string
  if (typeof timestamp === 'string') {
    return timestamp;
  }
  
  // Date JavaScript
  if (timestamp instanceof Date) {
    return timestamp.toISOString();
  }
  
  return new Date().toISOString();
};

// ✅ BON : Utilisation dans conversion
const firestoreToQuote = (id: string, data: any): Quote => ({
  ...data,
  id,
  createdAt: timestampToISO(data.createdAt),
  updatedAt: timestampToISO(data.updatedAt),
});

8. Gestion des Erreurs Firebase

typescript
// ✅ BON : Gestion d'erreurs complète
const handleFirebaseError = (error: any): string => {
  const code = error?.code || '';
  
  const errorMessages: Record<string, string> = {
    'permission-denied': 'Vous n\'avez pas la permission d\'effectuer cette action',
    'not-found': 'Document non trouvé',
    'already-exists': 'Ce document existe déjà',
    'resource-exhausted': 'Quota Firebase dépassé',
    'unavailable': 'Service Firebase temporairement indisponible',
    'cancelled': 'Opération annulée',
    'invalid-argument': 'Données invalides',
    'failed-precondition': 'Opération impossible dans l\'état actuel',
  };
  
  return errorMessages[code] || 'Une erreur est survenue';
};

// Utilisation
try {
  await createQuote(data);
} catch (error) {
  const message = handleFirebaseError(error);
  toast.error(message);
}

🚫 Ce que l'agent NE DOIT PAS faire

❌ Anti-patterns critiques

1. Ne JAMAIS oublier de désabonner les listeners

typescript
// ❌ MAUVAIS : Fuite mémoire
useEffect(() => {
  onSnapshot(collection(db, 'tasks'), (snapshot) => {
    // ...
  });
}, []); // Pas de cleanup !

// ✅ BON : Cleanup dans useEffect
useEffect(() => {
  const unsubscribe = onSnapshot(collection(db, 'tasks'), (snapshot) => {
    // ...
  });
  
  return () => unsubscribe(); // Cleanup !
}, []);

2. Ne PAS stocker de données sensibles sans chiffrement

typescript
// ❌ MAUVAIS : Données sensibles en clair
await setDoc(doc(db, 'users', id), {
  password: 'secret123',  // JAMAIS !
  creditCard: '4111...',  // JAMAIS !
});

// ✅ BON : Pas de données sensibles dans Firestore
// Les mots de passe sont gérés par Firebase Auth
// Les paiements par Stripe/autre service externe

3. Ne PAS ignorer les limites Firestore

typescript
// ❌ MAUVAIS : Query sans limite
const getAllQuotes = async () => {
  const snapshot = await getDocs(collection(db, 'quotes'));
  return snapshot.docs; // Peut retourner 10000+ docs !
};

// ✅ BON : Toujours limiter
import { limit } from 'firebase/firestore';

const getRecentQuotes = async (count = 50) => {
  const q = query(
    collection(db, 'quotes'),
    orderBy('createdAt', 'desc'),
    limit(count)
  );
  const snapshot = await getDocs(q);
  return snapshot.docs;
};

4. Ne PAS faire de queries composées invalides

typescript
// ❌ MAUVAIS : Firestore ne supporte pas
const q = query(
  collection(db, 'quotes'),
  where('status', '==', 'published'),
  where('category', '!=', 'sport'),  // != avec autre where
  orderBy('createdAt')  // Erreur : doit d'abord orderBy 'category'
);

// ✅ BON : Respecter les limitations Firestore
// - Un seul != ou not-in par query
// - orderBy sur le champ du where si inégalité
// - Index composés nécessaires pour certaines queries

5. Ne PAS mettre undefined dans Firestore

typescript
// ❌ MAUVAIS : undefined cause une erreur
await setDoc(docRef, {
  title: 'Ma citation',
  category: undefined,  // Erreur Firestore !
});

// ✅ BON : Nettoyer les undefined
const removeUndefined = <T extends object>(obj: T): Partial<T> => {
  return Object.fromEntries(
    Object.entries(obj).filter(([_, v]) => v !== undefined)
  ) as Partial<T>;
};

await setDoc(docRef, removeUndefined({
  title: 'Ma citation',
  category: undefined,  // Sera supprimé
}));

6. Ne PAS créer de listeners multiples identiques

typescript
// ❌ MAUVAIS : Nouveau listener à chaque appel
const subscribeToTasks = () => {
  onSnapshot(collection(db, 'tasks'), (snapshot) => {
    // Crée un NOUVEAU listener à chaque appel !
  });
};

// ✅ BON : Vérifier et désabonner l'ancien
let currentUnsubscribe: (() => void) | null = null;

const subscribeToTasks = () => {
  if (currentUnsubscribe) {
    currentUnsubscribe();  // Désabonner l'ancien
  }
  
  currentUnsubscribe = onSnapshot(collection(db, 'tasks'), (snapshot) => {
    // ...
  });
};

📊 Structure des Documents

Quote (citations)

typescript
interface QuoteDocument {
  // Identifiant (auto-généré)
  id: string;
  
  // Contenu
  content: string;
  source: {
    type: 'live' | 'archive' | 'external' | 'manual';
    audioUrl?: string;
    timestamp?: string;
  };
  
  // Auteur
  author: {
    id?: string;
    name: string;
    role?: string;
    avatar?: string;
  };
  
  // Contexte
  context?: {
    emissionId?: string;
    showPlanId?: string;
    showName?: string;
    date?: Timestamp;
  };
  
  // Métadonnées
  metadata?: {
    category?: string;
    tags?: string[];
    importance?: 'low' | 'medium' | 'high';
  };
  
  // Statut
  status: 'draft' | 'published' | 'archived';
  
  // Audit
  createdAt: Timestamp;
  updatedAt: Timestamp;
  createdBy: string;
}

Task (tâches)

typescript
interface TaskDocument {
  id: string;
  title: string;
  description?: string;
  status: 'todo' | 'in-progress' | 'done';
  priority: 'low' | 'medium' | 'high';
  assigneeIds: string[];
  dueDate?: string;
  tags: string[];
  comments: TaskComment[];
  createdAt: string;
  updatedAt: string;
  createdBy: string;
}

ChatRoom (salon)

typescript
interface ChatRoomDocument {
  id: string;
  name: string;
  type: 'direct' | 'group' | 'channel';
  participants: string[];
  createdBy: string;
  createdAt: Timestamp;
  updatedAt: Timestamp;
  lastMessage?: {
    content: string;
    senderId: string;
    timestamp: Timestamp;
  };
  unreadCount: Record<string, number>;
}

Message (sous-collection de rooms)

typescript
interface MessageDocument {
  id: string;
  content: string;
  senderId: string;
  senderName: string;
  timestamp: Timestamp;
  type: 'text' | 'file' | 'image' | 'system';
  fileUrl?: string;
  fileName?: string;
  reactions?: Record<string, string[]>;  // { "👍": ["user1", "user2"] }
  editedAt?: Timestamp;
  replyTo?: string;
}

🔒 Règles de Sécurité Firestore

Exemple de règles recommandées

javascript
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    // Fonction helper : utilisateur authentifié
    function isAuthenticated() {
      return request.auth != null;
    }
    
    // Fonction helper : propriétaire du document
    function isOwner(userId) {
      return request.auth.uid == userId;
    }
    
    // Quotes
    match /quotes/{quoteId} {
      allow read: if isAuthenticated();
      allow create: if isAuthenticated() 
        && request.resource.data.createdBy == request.auth.uid;
      allow update: if isAuthenticated() 
        && (resource.data.createdBy == request.auth.uid 
            || request.auth.token.role == 'admin');
      allow delete: if isAuthenticated() 
        && request.auth.token.role == 'admin';
    }
    
    // Tasks
    match /tasks/{taskId} {
      allow read, write: if isAuthenticated();
    }
    
    // Chat Rooms
    match /rooms/{roomId} {
      allow read: if isAuthenticated() 
        && request.auth.uid in resource.data.participants;
      allow create: if isAuthenticated();
      allow update, delete: if isAuthenticated() 
        && request.auth.uid in resource.data.participants;
      
      // Messages (sous-collection)
      match /messages/{messageId} {
        allow read: if isAuthenticated();
        allow create: if isAuthenticated() 
          && request.resource.data.senderId == request.auth.uid;
        allow update, delete: if isAuthenticated() 
          && resource.data.senderId == request.auth.uid;
      }
    }
    
    // User Preferences
    match /userPreferences/{userId} {
      allow read, write: if isAuthenticated() && isOwner(userId);
    }
  }
}

✅ Checklist Firebase

Configuration

  • Variables d'environnement Firebase configurées
  • firebase.ts exporte db, auth, storage
  • Règles de sécurité Firestore déployées

Opérations CRUD

  • serverTimestamp() pour createdAt/updatedAt
  • Conversion Timestamp → ISO string pour le frontend
  • Gestion des erreurs avec messages user-friendly
  • Pas de undefined dans les documents

Temps Réel

  • Listeners désabonnés au cleanup
  • Pas de listeners dupliqués
  • Gestion des erreurs de subscription

Storage

  • Validation taille fichier avant upload
  • Validation type MIME
  • Suppression fichiers orphelins

Performance

  • Queries avec limit()
  • Index composés créés si nécessaire
  • Batch pour opérations multiples

🚀 Exemples de requêtes utilisateur

code
✅ "Ajoute un listener sur les tasks"
✅ "Comment uploader un fichier audio ?"
✅ "Crée une fonction pour supprimer une room et ses messages"
✅ "Convertis ce timestamp Firestore"
✅ "Ajoute une citation dans Firestore"
✅ "Comment gérer les erreurs Firebase ?"
✅ "Crée un batch pour supprimer plusieurs documents"

📚 Ressources


📝 Métadonnées

  • Version: 1.0.0
  • Dernière mise à jour: 2026-02-03
  • Priorité: Haute
  • Dépendances: project-overview, coding-standards, security
  • Utilisé par: Modules Chat, Tasks, Quotes, UserPreferences, Versions
  • Collections: rooms, tasks, quotes, userPreferences, versions