# Fichier: python_cheats/cheatsheets/firebase.txt
# Cheatsheet Firebase - Guide Complet pour Débutants


[OK] QU'EST-CE QUE FIREBASE ?

Firebase est une plateforme de développement d'applications mobiles et web créée par Google.
Elle offre des services backend prêts à l'emploi pour accélérer le développement.

# Services principaux:
- Authentication (connexion utilisateurs)
- Firestore (base de données NoSQL en temps réel)
- Realtime Database (base de données JSON temps réel)
- Cloud Storage (stockage de fichiers)
- Cloud Functions (code backend serverless)
- Hosting (hébergement web)
- Cloud Messaging (notifications push)
- Analytics (statistiques d'utilisation)
- Crashlytics (rapports de bugs)

# Avantages:
[OK] Pas besoin de gérer des serveurs
[OK] Synchronisation temps réel
[OK] Gratuit jusqu'à un certain volume (plan Spark)
[OK] Scalabilité automatique
[OK] Intégration facile avec applications web/mobile

# Cas d'usage:
- Applications chat en temps réel
- Applications collaboratives
- E-commerce
- Réseaux sociaux
- Applications mobiles avec backend
- Sites web dynamiques


[OK] INSTALLATION & CONFIGURATION INITIALE


# === PRÉREQUIS ===

# Compte Google requis
# Créer compte sur: https://console.firebase.google.com

# Node.js et npm (pour Firebase CLI)
# Vérifier installation:
node --version                  # v14.0.0 ou supérieur
npm --version                   # v6.0.0 ou supérieur

# === INSTALLER FIREBASE CLI ===

# Installation globale (recommandé)
npm install -g firebase-tools

# Vérifier installation
firebase --version
firebase -V

# Mettre à jour Firebase CLI
npm install -g firebase-tools@latest
npm update -g firebase-tools

# Installation locale dans projet (alternative)
npm install --save-dev firebase-tools
npx firebase --version

# === CONNEXION À FIREBASE ===

# Se connecter avec compte Google
firebase login

# Se connecter (ouvre navigateur)
firebase login --interactive

# Se connecter sans navigateur (CI/CD)
firebase login --no-localhost

# Vérifier connexion
firebase login:list

# Se déconnecter
firebase logout

# === CRÉER PROJET FIREBASE ===

# Via Console Web (recommandé pour débuter)
# 1. Aller sur https://console.firebase.google.com
# 2. Cliquer "Ajouter un projet"
# 3. Nom du projet: myapp
# 4. Activer/désactiver Google Analytics
# 5. Choisir compte Analytics si activé

# Via CLI (après création console)
firebase projects:list                  # Lister projets
firebase use myapp-12345               # Sélectionner projet
firebase use --add                     # Ajouter alias pour projet


[OK] INITIALISER PROJET LOCAL


# === INITIALISATION COMPLÈTE ===

# Créer dossier projet
mkdir mon-app-firebase
cd mon-app-firebase

# Initialiser Firebase dans projet
firebase init

# Menu interactif:
# ? Which Firebase features? (Sélectionner avec espace)
#   [LARGE_CIRCLE] Realtime Database
#   [LARGE_CIRCLE] Firestore
#   [LARGE_CIRCLE] Functions
#   [LARGE_CIRCLE] Hosting
#   [LARGE_CIRCLE] Storage
#   [LARGE_CIRCLE] Emulators

# ? Select project: (Choisir projet existant ou créer nouveau)

# ? What file should be used for Firestore Rules? firestore.rules
# ? What file should be used for Firestore indexes? firestore.indexes.json

# ? What language would you like to use? (Functions)
#   JavaScript
#   TypeScript
#   Python (Beta)

# ? What do you want to use as your public directory? public
# ? Configure as a single-page app? Yes/No
# ? Set up automatic builds and deploys with GitHub? Yes/No

# === INITIALISATION SPÉCIFIQUE ===

# Initialiser seulement Hosting
firebase init hosting

# Initialiser seulement Firestore
firebase init firestore

# Initialiser seulement Functions
firebase init functions

# Initialiser plusieurs services
firebase init firestore hosting functions

# === STRUCTURE PROJET CRÉÉE ===

# Structure typique après init:
mon-app-firebase/
├── .firebaserc              # Configuration projets
├── firebase.json            # Configuration services
├── firestore.rules          # Règles sécurité Firestore
├── firestore.indexes.json   # Index Firestore
├── storage.rules            # Règles sécurité Storage
├── database.rules.json      # Règles Realtime Database
├── public/                  # Fichiers hosting
│   ├── index.html
│   └── 404.html
└── functions/               # Cloud Functions
    ├── package.json
    ├── index.js
    └── node_modules/

# === FICHIER .firebaserc ===

{
  "projects": {
    "default": "myapp-12345",
    "staging": "myapp-staging-67890",
    "production": "myapp-prod-11111"
  }
}

# Utilisation:
firebase use default
firebase use staging
firebase use production

# === FICHIER firebase.json ===

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "hosting": {
    "public": "public",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [{
      "source": "**",
      "destination": "/index.html"
    }]
  },
  "functions": [{
    "source": "functions",
    "codebase": "default",
    "ignore": [
      "node_modules",
      ".git",
      "firebase-debug.log"
    ]
  }],
  "storage": {
    "rules": "storage.rules"
  }
}


[OK] FIREBASE SDK - CONFIGURATION WEB


# === INSTALLATION SDK WEB ===

# Via npm (applications modernes)
npm install firebase

# Via CDN (HTML simple)
# Voir console Firebase pour URLs à jour

# === INITIALISATION SDK (JavaScript/ES6) ===

// firebase-config.js
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';
import { getFunctions } from 'firebase/functions';

// Configuration (obtenir depuis Console Firebase)
const firebaseConfig = {
  apiKey: "AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  authDomain: "myapp-12345.firebaseapp.com",
  projectId: "myapp-12345",
  storageBucket: "myapp-12345.appspot.com",
  messagingSenderId: "123456789012",
  appId: "1:123456789012:web:abcdef123456",
  measurementId: "G-XXXXXXXXXX"  // Optionnel (Analytics)
};

// Initialiser Firebase
const app = initializeApp(firebaseConfig);

// Initialiser services
export const auth = getAuth(app);
export const db = getFirestore(app);
export const storage = getStorage(app);
export const functions = getFunctions(app);

// === OBTENIR CONFIGURATION ===

# Via Console Firebase:
# 1. Sélectionner projet
# 2. Paramètres projet (roue dentée)
# 3. Vos applications
# 4. Ajouter une application (Web, iOS, Android)
# 5. Copier l'objet firebaseConfig

# Via CLI
firebase apps:sdkconfig web

# === SDK VERSION 8 (ANCIENNE - Compatibilité) ===

// Version 8 (namespace)
import firebase from 'firebase/app';
import 'firebase/auth';
import 'firebase/firestore';
import 'firebase/storage';

firebase.initializeApp(firebaseConfig);

const auth = firebase.auth();
const db = firebase.firestore();
const storage = firebase.storage();

# === UTILISATION CDN (HTML) ===

<!DOCTYPE html>
<html>
<head>
  <title>Mon App Firebase</title>
</head>
<body>
  <!-- Firebase SDK v9+ (modular) -->
  <script type="module">
    import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
    import { getAuth } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-auth.js';
    import { getFirestore } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-firestore.js';

    const firebaseConfig = { /* ... */ };
    const app = initializeApp(firebaseConfig);
    const auth = getAuth(app);
    const db = getFirestore(app);
  </script>
</body>
</html>


[OK] FIREBASE AUTHENTICATION


# === MÉTHODES D'AUTHENTIFICATION ===

# Disponibles:
- Email/Password (classique)
- Google
- Facebook
- Twitter
- GitHub
- Apple
- Microsoft
- Yahoo
- Téléphone (SMS)
- Anonyme
- Custom (votre propre système)

# === ACTIVER MÉTHODES (Console) ===

# 1. Console Firebase > Authentication
# 2. Onglet "Sign-in method"
# 3. Activer méthodes souhaitées
# 4. Configurer (OAuth pour réseaux sociaux)

# === EMAIL/PASSWORD ===

// Importer
import { 
  getAuth, 
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
  sendPasswordResetEmail,
  updateProfile
} from 'firebase/auth';

const auth = getAuth();

// === CRÉER COMPTE ===

// Inscription basique
async function register(email, password) {
  try {
    const userCredential = await createUserWithEmailAndPassword(
      auth, 
      email, 
      password
    );
    const user = userCredential.user;
    console.log('Utilisateur créé:', user.uid);
    return user;
  } catch (error) {
    console.error('Erreur inscription:', error.code, error.message);
    // Gérer erreurs
    if (error.code === 'auth/email-already-in-use') {
      alert('Email déjà utilisé');
    } else if (error.code === 'auth/weak-password') {
      alert('Mot de passe trop faible');
    }
  }
}

// Utilisation
register('user@example.com', 'password123');

// === CONNEXION ===

// Connexion basique
async function login(email, password) {
  try {
    const userCredential = await signInWithEmailAndPassword(
      auth, 
      email, 
      password
    );
    const user = userCredential.user;
    console.log('Connecté:', user.email);
    return user;
  } catch (error) {
    console.error('Erreur connexion:', error.code);
    if (error.code === 'auth/wrong-password') {
      alert('Mot de passe incorrect');
    } else if (error.code === 'auth/user-not-found') {
      alert('Utilisateur non trouvé');
    }
  }
}

// Utilisation
login('user@example.com', 'password123');

// === DÉCONNEXION ===

async function logout() {
  try {
    await signOut(auth);
    console.log('Déconnecté');
  } catch (error) {
    console.error('Erreur déconnexion:', error);
  }
}

// Utilisation
logout();

// === OBSERVER ÉTAT CONNEXION ===

// Écouter changements d'état (IMPORTANT!)
onAuthStateChanged(auth, (user) => {
  if (user) {
    // Utilisateur connecté
    console.log('Utilisateur:', user.uid, user.email);
    console.log('Email vérifié:', user.emailVerified);
    
    // Afficher contenu protégé
    showUserContent();
  } else {
    // Utilisateur déconnecté
    console.log('Non connecté');
    
    // Rediriger vers page login
    showLoginPage();
  }
});

// === INFORMATIONS UTILISATEUR ===

// Obtenir utilisateur actuel
const currentUser = auth.currentUser;

if (currentUser) {
  // Propriétés disponibles
  const uid = currentUser.uid;              // ID unique
  const email = currentUser.email;          // Email
  const displayName = currentUser.displayName;  // Nom affiché
  const photoURL = currentUser.photoURL;    // Photo profil
  const emailVerified = currentUser.emailVerified;  // Email vérifié?
  const metadata = currentUser.metadata;    // Métadonnées
  
  console.log('User ID:', uid);
  console.log('Email:', email);
  console.log('Nom:', displayName);
  console.log('Photo:', photoURL);
  console.log('Création:', metadata.creationTime);
  console.log('Dernière connexion:', metadata.lastSignInTime);
}

// === METTRE À JOUR PROFIL ===

async function updateUserProfile(displayName, photoURL) {
  try {
    await updateProfile(auth.currentUser, {
      displayName: displayName,
      photoURL: photoURL
    });
    console.log('Profil mis à jour');
  } catch (error) {
    console.error('Erreur mise à jour:', error);
  }
}

// Utilisation
updateUserProfile('Jean Dupont', 'https://example.com/photo.jpg');

// === RÉINITIALISER MOT DE PASSE ===

async function resetPassword(email) {
  try {
    await sendPasswordResetEmail(auth, email);
    console.log('Email de réinitialisation envoyé');
    alert('Vérifiez votre email');
  } catch (error) {
    console.error('Erreur:', error.code);
  }
}

// Utilisation
resetPassword('user@example.com');

// === VÉRIFICATION EMAIL ===

import { sendEmailVerification } from 'firebase/auth';

async function verifyEmail() {
  try {
    await sendEmailVerification(auth.currentUser);
    console.log('Email de vérification envoyé');
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// === CONNEXION GOOGLE ===

import { 
  GoogleAuthProvider, 
  signInWithPopup,
  signInWithRedirect,
  getRedirectResult 
} from 'firebase/auth';

// Popup (recommandé)
async function loginWithGoogle() {
  const provider = new GoogleAuthProvider();
  
  try {
    const result = await signInWithPopup(auth, provider);
    const user = result.user;
    const credential = GoogleAuthProvider.credentialFromResult(result);
    const token = credential.accessToken;
    
    console.log('Connecté avec Google:', user.email);
    return user;
  } catch (error) {
    console.error('Erreur Google Auth:', error.code);
  }
}

// Redirection (mobile)
async function loginWithGoogleRedirect() {
  const provider = new GoogleAuthProvider();
  await signInWithRedirect(auth, provider);
}

// Récupérer résultat après redirection
async function handleRedirectResult() {
  try {
    const result = await getRedirectResult(auth);
    if (result) {
      const user = result.user;
      console.log('Connecté:', user.email);
    }
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Options supplémentaires
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
provider.setCustomParameters({
  'login_hint': 'user@example.com'
});

// === CONNEXION FACEBOOK ===

import { FacebookAuthProvider } from 'firebase/auth';

async function loginWithFacebook() {
  const provider = new FacebookAuthProvider();
  provider.addScope('user_birthday');
  
  try {
    const result = await signInWithPopup(auth, provider);
    const user = result.user;
    console.log('Connecté avec Facebook:', user.displayName);
    return user;
  } catch (error) {
    console.error('Erreur Facebook Auth:', error);
  }
}

// === CONNEXION ANONYME ===

import { signInAnonymously } from 'firebase/auth';

async function loginAnonymously() {
  try {
    const result = await signInAnonymously(auth);
    console.log('Connecté anonymement:', result.user.uid);
    return result.user;
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Convertir compte anonyme en permanent
import { linkWithCredential, EmailAuthProvider } from 'firebase/auth';

async function linkAnonymousAccount(email, password) {
  const credential = EmailAuthProvider.credential(email, password);
  
  try {
    const result = await linkWithCredential(auth.currentUser, credential);
    console.log('Compte lié:', result.user.email);
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// === AUTHENTIFICATION TÉLÉPHONE ===

import { 
  RecaptchaVerifier, 
  signInWithPhoneNumber 
} from 'firebase/auth';

// Configuration reCAPTCHA
window.recaptchaVerifier = new RecaptchaVerifier(auth, 'recaptcha-container', {
  'size': 'invisible',
  'callback': (response) => {
    // reCAPTCHA résolu
  }
});

// Envoyer code SMS
async function sendSMSCode(phoneNumber) {
  const appVerifier = window.recaptchaVerifier;
  
  try {
    const confirmationResult = await signInWithPhoneNumber(
      auth, 
      phoneNumber, 
      appVerifier
    );
    
    window.confirmationResult = confirmationResult;
    console.log('Code SMS envoyé');
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Vérifier code
async function verifyCode(code) {
  try {
    const result = await window.confirmationResult.confirm(code);
    const user = result.user;
    console.log('Connecté:', user.phoneNumber);
  } catch (error) {
    console.error('Code invalide:', error);
  }
}

// Utilisation
sendSMSCode('+33612345678');
// Utilisateur reçoit code
verifyCode('123456');

// === GESTION ERREURS AUTHENTICATION ===

// Codes erreurs courants:
// auth/email-already-in-use
// auth/invalid-email
// auth/operation-not-allowed
// auth/weak-password
// auth/user-disabled
// auth/user-not-found
// auth/wrong-password
// auth/network-request-failed
// auth/too-many-requests

function handleAuthError(error) {
  switch (error.code) {
    case 'auth/email-already-in-use':
      return 'Cet email est déjà utilisé';
    case 'auth/invalid-email':
      return 'Email invalide';
    case 'auth/weak-password':
      return 'Mot de passe trop faible (min 6 caractères)';
    case 'auth/user-not-found':
      return 'Utilisateur non trouvé';
    case 'auth/wrong-password':
      return 'Mot de passe incorrect';
    case 'auth/too-many-requests':
      return 'Trop de tentatives, réessayez plus tard';
    default:
      return 'Erreur: ' + error.message;
  }
}


[OK] CLOUD FIRESTORE (BASE DE DONNÉES)


# === QU'EST-CE QUE FIRESTORE? ===

Firestore est une base de données NoSQL flexible, scalable et en temps réel.

# Structure:
# Collections -> Documents -> Champs (+ sous-collections)

# Exemple:
users (collection)
  └── user123 (document)
      ├── name: "Jean"
      ├── email: "jean@example.com"
      └── posts (sous-collection)
          └── post456 (document)
              ├── title: "Mon article"
              └── content: "..."

# === ACTIVER FIRESTORE ===

# Via Console Firebase:
# 1. Firestore Database
# 2. Créer une base de données
# 3. Mode production (règles strictes) ou Mode test (règles permissives)
# 4. Choisir région (ex: europe-west1)

# Via CLI:
firebase init firestore

# === IMPORTER SDK ===

import { 
  getFirestore,
  collection,
  doc,
  addDoc,
  setDoc,
  getDoc,
  getDocs,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  limit,
  onSnapshot
} from 'firebase/firestore';

const db = getFirestore();

# === CRÉER/AJOUTER DOCUMENTS ===

// Ajouter document avec ID auto-généré
async function addUser(name, email, age) {
  try {
    const docRef = await addDoc(collection(db, 'users'), {
      name: name,
      email: email,
      age: age,
      createdAt: new Date(),
      active: true
    });
    
    console.log('Document créé avec ID:', docRef.id);
    return docRef.id;
  } catch (error) {
    console.error('Erreur ajout:', error);
  }
}

// Utilisation
addUser('Jean Dupont', 'jean@example.com', 30);

// Créer document avec ID personnalisé
async function createUser(userId, userData) {
  try {
    await setDoc(doc(db, 'users', userId), {
      name: userData.name,
      email: userData.email,
      age: userData.age,
      createdAt: new Date()
    });
    
    console.log('Document créé avec ID:', userId);
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Utilisation
createUser('user123', {
  name: 'Marie Martin',
  email: 'marie@example.com',
  age: 25
});

// Fusionner avec document existant
await setDoc(doc(db, 'users', 'user123'), {
  lastLogin: new Date()
}, { merge: true });  // Préserve champs existants

# === LIRE DOCUMENTS ===

// Lire un document spécifique
async function getUser(userId) {
  try {
    const docRef = doc(db, 'users', userId);
    const docSnap = await getDoc(docRef);
    
    if (docSnap.exists()) {
      console.log('Données:', docSnap.data());
      return docSnap.data();
    } else {
      console.log('Document non trouvé');
      return null;
    }
  } catch (error) {
    console.error('Erreur lecture:', error);
  }
}

// Utilisation
const userData = await getUser('user123');
console.log(userData.name, userData.email);

// Lire tous les documents d'une collection
async function getAllUsers() {
  try {
    const querySnapshot = await getDocs(collection(db, 'users'));
    
    const users = [];
    querySnapshot.forEach((doc) => {
      users.push({
        id: doc.id,
        ...doc.data()
      });
    });
    
    console.log('Nombre utilisateurs:', users.length);
    return users;
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Utilisation
const allUsers = await getAllUsers();
allUsers.forEach(user => {
  console.log(user.id, user.name);
});

# === METTRE À JOUR DOCUMENTS ===

// Mettre à jour champs existants
async function updateUser(userId, updates) {
  try {
    const docRef = doc(db, 'users', userId);
    await updateDoc(docRef, updates);
    
    console.log('Document mis à jour');
  } catch (error) {
    console.error('Erreur mise à jour:', error);
  }
}

// Utilisation
updateUser('user123', {
  age: 31,
  city: 'Paris',
  lastModified: new Date()
});

// Incrémenter valeur numérique
import { increment } from 'firebase/firestore';

await updateDoc(doc(db, 'users', 'user123'), {
  loginCount: increment(1),      // +1
  points: increment(10)          // +10
});

// Ajouter élément à tableau
import { arrayUnion, arrayRemove } from 'firebase/firestore';

await updateDoc(doc(db, 'users', 'user123'), {
  hobbies: arrayUnion('lecture', 'sport')  // Ajoute si pas déjà présent
});

// Retirer élément de tableau
await updateDoc(doc(db, 'users', 'user123'), {
  hobbies: arrayRemove('lecture')
});

// Supprimer champ
import { deleteField } from 'firebase/firestore';

await updateDoc(doc(db, 'users', 'user123'), {
  tempField: deleteField()
});

# === SUPPRIMER DOCUMENTS ===

// Supprimer document
async function deleteUser(userId) {
  try {
    await deleteDoc(doc(db, 'users', userId));
    console.log('Document supprimé');
  } catch (error) {
    console.error('Erreur suppression:', error);
  }
}

// Utilisation
deleteUser('user123');

// NOTE: Supprimer document ne supprime PAS les sous-collections!
// Il faut les supprimer manuellement

# === REQUÊTES FIRESTORE ===

// Filtrer avec where()
async function getUsersByAge(minAge) {
  const q = query(
    collection(db, 'users'),
    where('age', '>=', minAge)
  );
  
  const querySnapshot = await getDocs(q);
  const users = [];
  querySnapshot.forEach((doc) => {
    users.push({ id: doc.id, ...doc.data() });
  });
  
  return users;
}

// Utilisation
const adults = await getUsersByAge(18);

// Opérateurs where:
// == (égal)
// != (différent)
// > (supérieur)
// >= (supérieur ou égal)
// < (inférieur)
// <= (inférieur ou égal)
// in (dans tableau)
// not-in (pas dans tableau)
// array-contains (tableau contient)
// array-contains-any (tableau contient au moins un)

// Exemples de requêtes:

// Égalité
where('city', '==', 'Paris')

// Plusieurs conditions
query(
  collection(db, 'users'),
  where('age', '>=', 18),
  where('age', '<=', 65),
  where('city', '==', 'Paris')
)

// In (max 10 valeurs)
where('status', 'in', ['active', 'pending', 'verified'])

// Array contains
where('hobbies', 'array-contains', 'sport')

// Array contains any
where('tags', 'array-contains-any', ['urgent', 'important'])

// Trier avec orderBy()
query(
  collection(db, 'users'),
  orderBy('age', 'desc'),      // Décroissant
  orderBy('name', 'asc')       // Croissant
)

// Limiter résultats
query(
  collection(db, 'users'),
  orderBy('createdAt', 'desc'),
  limit(10)                    // 10 premiers résultats
)

// Requête complète
async function getRecentUsers(count) {
  const q = query(
    collection(db, 'users'),
    where('active', '==', true),
    orderBy('createdAt', 'desc'),
    limit(count)
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }));
}

# === PAGINATION ===

import { startAfter, startAt, endBefore, limitToLast } from 'firebase/firestore';

// Page suivante
async function getNextPage(lastDoc) {
  const q = query(
    collection(db, 'users'),
    orderBy('name'),
    startAfter(lastDoc),
    limit(10)
  );
  
  const snapshot = await getDocs(q);
  return snapshot.docs;
}

// Utilisation pagination:
let lastVisible = null;

// Première page
const firstQuery = query(
  collection(db, 'users'),
  orderBy('name'),
  limit(10)
);
const firstSnapshot = await getDocs(firstQuery);
lastVisible = firstSnapshot.docs[firstSnapshot.docs.length - 1];

// Page suivante
const nextQuery = query(
  collection(db, 'users'),
  orderBy('name'),
  startAfter(lastVisible),
  limit(10)
);
const nextSnapshot = await getDocs(nextQuery);

# === ÉCOUTE TEMPS RÉEL (SNAPSHOT LISTENERS) ===

// Écouter document spécifique
function listenToUser(userId, callback) {
  const docRef = doc(db, 'users', userId);
  
  const unsubscribe = onSnapshot(docRef, (docSnap) => {
    if (docSnap.exists()) {
      console.log('Données mises à jour:', docSnap.data());
      callback(docSnap.data());
    } else {
      console.log('Document supprimé');
      callback(null);
    }
  }, (error) => {
    console.error('Erreur listener:', error);
  });
  
  return unsubscribe;  // Fonction pour arrêter l'écoute
}

// Utilisation
const unsubscribe = listenToUser('user123', (userData) => {
  if (userData) {
    document.getElementById('userName').textContent = userData.name;
  }
});

// Arrêter l'écoute quand nécessaire
unsubscribe();

// Écouter collection entière
function listenToUsers(callback) {
  const q = query(
    collection(db, 'users'),
    where('active', '==', true),
    orderBy('createdAt', 'desc')
  );
  
  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const users = [];
    querySnapshot.forEach((doc) => {
      users.push({ id: doc.id, ...doc.data() });
    });
    
    console.log('Utilisateurs actifs:', users.length);
    callback(users);
  });
  
  return unsubscribe;
}

// Détecter type de changement
onSnapshot(collection(db, 'users'), (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === 'added') {
      console.log('Nouveau:', change.doc.data());
    }
    if (change.type === 'modified') {
      console.log('Modifié:', change.doc.data());
    }
    if (change.type === 'removed') {
      console.log('Supprimé:', change.doc.data());
    }
  });
});

# === TRANSACTIONS ===

// Transactions pour opérations atomiques
import { runTransaction } from 'firebase/firestore';

async function transferPoints(fromUserId, toUserId, points) {
  try {
    await runTransaction(db, async (transaction) => {
      // Lire documents
      const fromRef = doc(db, 'users', fromUserId);
      const toRef = doc(db, 'users', toUserId);
      
      const fromDoc = await transaction.get(fromRef);
      const toDoc = await transaction.get(toRef);
      
      if (!fromDoc.exists() || !toDoc.exists()) {
        throw new Error('Utilisateur non trouvé');
      }
      
      const fromPoints = fromDoc.data().points || 0;
      
      if (fromPoints < points) {
        throw new Error('Points insuffisants');
      }
      
      // Écrire modifications
      transaction.update(fromRef, {
        points: fromPoints - points
      });
      
      transaction.update(toRef, {
        points: (toDoc.data().points || 0) + points
      });
    });
    
    console.log('Transaction réussie');
  } catch (error) {
    console.error('Transaction échouée:', error);
  }
}

// Utilisation
transferPoints('user123', 'user456', 100);

# === BATCH WRITES ===

// Écriture groupée (batch) - max 500 opérations
import { writeBatch } from 'firebase/firestore';

async function batchUpdateUsers(userIds, updates) {
  const batch = writeBatch(db);
  
  userIds.forEach((userId) => {
    const docRef = doc(db, 'users', userId);
    batch.update(docRef, updates);
  });
  
  try {
    await batch.commit();
    console.log('Batch réussi');
  } catch (error) {
    console.error('Erreur batch:', error);
  }
}

// Utilisation
batchUpdateUsers(['user1', 'user2', 'user3'], {
  verified: true,
  updatedAt: new Date()
});

// Batch avec différentes opérations
const batch = writeBatch(db);

// Set
batch.set(doc(db, 'users', 'user1'), { name: 'Alice' });

// Update
batch.update(doc(db, 'users', 'user2'), { age: 30 });

// Delete
batch.delete(doc(db, 'users', 'user3'));

await batch.commit();

# === SOUS-COLLECTIONS ===

// Structure hiérarchique
// users/{userId}/posts/{postId}

// Ajouter document dans sous-collection
async function addPost(userId, postData) {
  const postsRef = collection(db, 'users', userId, 'posts');
  
  const docRef = await addDoc(postsRef, {
    title: postData.title,
    content: postData.content,
    createdAt: new Date()
  });
  
  console.log('Post créé:', docRef.id);
  return docRef.id;
}

// Lire sous-collection
async function getUserPosts(userId) {
  const postsRef = collection(db, 'users', userId, 'posts');
  const q = query(postsRef, orderBy('createdAt', 'desc'));
  
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }));
}

// Utilisation
addPost('user123', {
  title: 'Mon premier article',
  content: 'Contenu...'
});

const posts = await getUserPosts('user123');

# === TYPES DE DONNÉES FIRESTORE ===

// Types supportés:
- String: "texte"
- Number: 123, 45.67
- Boolean: true, false
- Array: [1, 2, 3]
- Map (objet): { key: 'value' }
- Null: null
- Timestamp: new Date()
- GeoPoint: new GeoPoint(48.8566, 2.3522)
- Reference: doc(db, 'users', 'user123')
- Bytes: new Uint8Array([1, 2, 3])

// Exemple document complet
import { GeoPoint, Timestamp } from 'firebase/firestore';

await addDoc(collection(db, 'places'), {
  name: 'Tour Eiffel',
  location: new GeoPoint(48.8584, 2.2945),
  tags: ['monument', 'paris', 'tourisme'],
  metadata: {
    visitors: 7000000,
    height: 330,
    opened: 1889
  },
  lastUpdated: Timestamp.now(),
  active: true,
  description: 'Monument emblématique de Paris'
});

# === SÉCURITÉ FIRESTORE (RÈGLES) ===

// Fichier: firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    
    // === RÈGLES DE BASE ===
    
    // Tout le monde peut lire, personne ne peut écrire
    match /{document=**} {
      allow read: if true;
      allow write: if false;
    }
    
    // Authentification requise
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
    
    // === RÈGLES PAR COLLECTION ===
    
    // Users: chacun ne peut modifier que son propre profil
    match /users/{userId} {
      allow read: if request.auth != null;
      allow write: if request.auth != null && request.auth.uid == userId;
    }
    
    // Posts publics lisibles, création authentifiée
    match /posts/{postId} {
      allow read: if true;
      allow create: if request.auth != null;
      allow update, delete: if request.auth != null 
                             && request.auth.uid == resource.data.authorId;
    }
    
    // === VALIDATION DONNÉES ===
    
    match /users/{userId} {
      allow create: if request.auth != null
                    && request.auth.uid == userId
                    && request.resource.data.name is string
                    && request.resource.data.name.size() > 0
                    && request.resource.data.age is int
                    && request.resource.data.age >= 0
                    && request.resource.data.email is string;
      
      allow update: if request.auth != null
                    && request.auth.uid == userId
                    && request.resource.data.name is string
                    && request.resource.data.name.size() > 0;
    }
    
    // === SOUS-COLLECTIONS ===
    
    match /users/{userId}/posts/{postId} {
      allow read: if request.auth != null;
      allow write: if request.auth != null && request.auth.uid == userId;
    }
    
    // === FONCTIONS PERSONNALISÉES ===
    
    function isSignedIn() {
      return request.auth != null;
    }
    
    function isOwner(userId) {
      return request.auth.uid == userId;
    }
    
    function isValidUser(data) {
      return data.name is string
          && data.name.size() > 0
          && data.email is string;
    }
    
    match /users/{userId} {
      allow read: if isSignedIn();
      allow write: if isSignedIn() 
                   && isOwner(userId) 
                   && isValidUser(request.resource.data);
    }
    
    // === VÉRIFICATIONS AVANCÉES ===
    
    // Limiter taille tableau
    match /lists/{listId} {
      allow update: if request.resource.data.items.size() <= 100;
    }
    
    // Empêcher modification certain champ
    match /posts/{postId} {
      allow update: if request.resource.data.authorId == resource.data.authorId;
    }
    
    // Vérifier timestamp
    match /events/{eventId} {
      allow create: if request.resource.data.startDate > request.time;
    }
    
    // Rôles personnalisés
    match /admin/{document=**} {
      allow read, write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }
  }
}

// Déployer règles
firebase deploy --only firestore:rules

// Tester règles (Console Firebase)
// Onglet "Rules Playground" dans Firestore


[OK] CLOUD STORAGE (STOCKAGE FICHIERS)


# === QU'EST-CE QUE CLOUD STORAGE? ===

Service de stockage de fichiers (images, vidéos, PDF, etc.)

# Caractéristiques:
[OK] Scalable automatiquement
[OK] CDN intégré (rapide partout)
[OK] Sécurisé avec règles
[OK] Intégration Auth
[OK] Upload/download avec progression

# === ACTIVER STORAGE ===

# Via Console Firebase:
# 1. Storage
# 2. Commencer
# 3. Choisir règles (test ou production)
# 4. Choisir région

# Via CLI:
firebase init storage

# === IMPORTER SDK ===

import { 
  getStorage,
  ref,
  uploadBytes,
  uploadBytesResumable,
  uploadString,
  getDownloadURL,
  deleteObject,
  listAll,
  getMetadata,
  updateMetadata
} from 'firebase/storage';

const storage = getStorage();

# === UPLOAD FICHIERS ===

// Upload fichier simple
async function uploadFile(file) {
  const storageRef = ref(storage, 'images/' + file.name);
  
  try {
    const snapshot = await uploadBytes(storageRef, file);
    console.log('Fichier uploadé:', snapshot.metadata.fullPath);
    
    // Obtenir URL de téléchargement
    const url = await getDownloadURL(snapshot.ref);
    console.log('URL:', url);
    
    return url;
  } catch (error) {
    console.error('Erreur upload:', error);
  }
}

// Utilisation avec input file
document.getElementById('fileInput').addEventListener('change', (e) => {
  const file = e.target.files[0];
  if (file) {
    uploadFile(file);
  }
});

// Upload avec chemin personnalisé
async function uploadUserAvatar(userId, file) {
  const storageRef = ref(storage, `users/${userId}/avatar.jpg`);
  
  const snapshot = await uploadBytes(storageRef, file);
  const url = await getDownloadURL(snapshot.ref);
  
  // Sauvegarder URL dans Firestore
  await updateDoc(doc(db, 'users', userId), {
    photoURL: url
  });
  
  return url;
}

// Upload avec progression
function uploadFileWithProgress(file, onProgress) {
  const storageRef = ref(storage, 'uploads/' + file.name);
  const uploadTask = uploadBytesResumable(storageRef, file);
  
  uploadTask.on('state_changed',
    (snapshot) => {
      // Progression
      const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
      console.log('Upload:', progress.toFixed(2) + '%');
      
      if (onProgress) {
        onProgress(progress);
      }
      
      switch (snapshot.state) {
        case 'paused':
          console.log('Upload en pause');
          break;
        case 'running':
          console.log('Upload en cours');
          break;
      }
    },
    (error) => {
      // Erreur
      console.error('Erreur upload:', error.code);
    },
    async () => {
      // Succès
      const url = await getDownloadURL(uploadTask.snapshot.ref);
      console.log('Fichier disponible:', url);
    }
  );
  
  return uploadTask;  // Pour pause/resume/cancel
}

// Utilisation
const uploadTask = uploadFileWithProgress(file, (progress) => {
  document.getElementById('progress').value = progress;
});

// Contrôler upload
uploadTask.pause();
uploadTask.resume();
uploadTask.cancel();

// Upload string (base64, data URL)
async function uploadBase64Image(base64String, fileName) {
  const storageRef = ref(storage, 'images/' + fileName);
  
  // Data URL
  await uploadString(storageRef, base64String, 'data_url');
  
  // Base64
  // await uploadString(storageRef, base64String, 'base64');
  
  // Raw string
  // await uploadString(storageRef, rawString, 'raw');
  
  const url = await getDownloadURL(storageRef);
  return url;
}

// Upload avec métadonnées
async function uploadWithMetadata(file) {
  const storageRef = ref(storage, 'documents/' + file.name);
  
  const metadata = {
    contentType: file.type,
    customMetadata: {
      'uploadedBy': auth.currentUser.uid,
      'uploadDate': new Date().toISOString(),
      'description': 'Document important'
    }
  };
  
  const snapshot = await uploadBytes(storageRef, file, metadata);
  console.log('Upload avec métadonnées');
}

# === TÉLÉCHARGER FICHIERS ===

// Obtenir URL de téléchargement
async function getFileURL(filePath) {
  const storageRef = ref(storage, filePath);
  
  try {
    const url = await getDownloadURL(storageRef);
    console.log('URL:', url);
    
    // Utiliser URL
    document.getElementById('image').src = url;
    
    return url;
  } catch (error) {
    if (error.code === 'storage/object-not-found') {
      console.log('Fichier non trouvé');
    }
    console.error('Erreur:', error);
  }
}

// Utilisation
getFileURL('images/photo.jpg');

// Télécharger données fichier
import { getBlob, getBytes } from 'firebase/storage';

// Comme Blob
async function downloadAsBlob(filePath) {
  const storageRef = ref(storage, filePath);
  const blob = await getBlob(storageRef);
  
  // Créer URL local
  const url = URL.createObjectURL(blob);
  
  // Télécharger
  const a = document.createElement('a');
  a.href = url;
  a.download = 'fichier.pdf';
  a.click();
}

// Comme Uint8Array
async function downloadAsBytes(filePath) {
  const storageRef = ref(storage, filePath);
  const bytes = await getBytes(storageRef);
  console.log('Taille:', bytes.length);
}

# === SUPPRIMER FICHIERS ===

// Supprimer fichier
async function deleteFile(filePath) {
  const storageRef = ref(storage, filePath);
  
  try {
    await deleteObject(storageRef);
    console.log('Fichier supprimé');
  } catch (error) {
    console.error('Erreur suppression:', error);
  }
}

// Utilisation
deleteFile('images/old-photo.jpg');

# === LISTER FICHIERS ===

// Lister fichiers dans dossier
async function listFiles(folderPath) {
  const folderRef = ref(storage, folderPath);
  
  try {
    const result = await listAll(folderRef);
    
    // Fichiers
    console.log('Fichiers:', result.items.length);
    result.items.forEach((itemRef) => {
      console.log('Fichier:', itemRef.fullPath);
    });
    
    // Sous-dossiers
    console.log('Dossiers:', result.prefixes.length);
    result.prefixes.forEach((folderRef) => {
      console.log('Dossier:', folderRef.fullPath);
    });
    
    return result;
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Utilisation
listFiles('images/');

// Obtenir URLs de tous les fichiers
async function getAllFileURLs(folderPath) {
  const folderRef = ref(storage, folderPath);
  const result = await listAll(folderRef);
  
  const urls = await Promise.all(
    result.items.map(item => getDownloadURL(item))
  );
  
  return urls;
}

# === MÉTADONNÉES ===

// Lire métadonnées
async function getFileMetadata(filePath) {
  const storageRef = ref(storage, filePath);
  
  try {
    const metadata = await getMetadata(storageRef);
    
    console.log('Nom:', metadata.name);
    console.log('Taille:', metadata.size, 'octets');
    console.log('Type:', metadata.contentType);
    console.log('Créé:', metadata.timeCreated);
    console.log('Modifié:', metadata.updated);
    console.log('URL:', metadata.fullPath);
    console.log('Custom:', metadata.customMetadata);
    
    return metadata;
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Mettre à jour métadonnées
async function updateFileMetadata(filePath) {
  const storageRef = ref(storage, filePath);
  
  const newMetadata = {
    contentType: 'image/jpeg',
    customMetadata: {
      'location': 'Paris',
      'camera': 'Canon EOS'
    }
  };
  
  try {
    const metadata = await updateMetadata(storageRef, newMetadata);
    console.log('Métadonnées mises à jour');
  } catch (error) {
    console.error('Erreur:', error);
  }
}

// Supprimer métadonnées custom
await updateMetadata(storageRef, {
  customMetadata: {
    'location': null  // Supprime ce champ
  }
});

# === SÉCURITÉ STORAGE (RÈGLES) ===

// Fichier: storage.rules

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    
    // === RÈGLES DE BASE ===
    
    // Authentification requise pour lecture/écriture
    match /{allPaths=**} {
      allow read, write: if request.auth != null;
    }
    
    // Lecture publique, écriture authentifiée
    match /{allPaths=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
    
    // === RÈGLES PAR DOSSIER ===
    
    // Avatars utilisateurs
    match /users/{userId}/avatar.jpg {
      allow read: if true;
      allow write: if request.auth != null 
                   && request.auth.uid == userId;
    }
    
    // Documents privés
    match /users/{userId}/documents/{document} {
      allow read, write: if request.auth != null 
                         && request.auth.uid == userId;
    }
    
    // Images publiques
    match /public/{allPaths=**} {
      allow read: if true;
      allow write: if request.auth != null;
    }
    
    // === VALIDATION FICHIERS ===
    
    // Limiter taille (max 5MB)
    match /images/{imageId} {
      allow write: if request.auth != null
                   && request.resource.size < 5 * 1024 * 1024;
    }
    
    // Vérifier type de fichier
    match /images/{imageId} {
      allow write: if request.auth != null
                   && request.resource.contentType.matches('image/.*');
    }
    
    // Types spécifiques
    match /documents/{docId} {
      allow write: if request.auth != null
                   && request.resource.contentType in [
                     'application/pdf',
                     'application/msword',
                     'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
                   ];
    }
    
    // === FONCTIONS PERSONNALISÉES ===
    
    function isSignedIn() {
      return request.auth != null;
    }
    
    function isOwner(userId) {
      return request.auth.uid == userId;
    }
    
    function isImage() {
      return request.resource.contentType.matches('image/.*');
    }
    
    function isSizeValid(maxSize) {
      return request.resource.size <= maxSize;
    }
    
    match /users/{userId}/photos/{photoId} {
      allow read: if isSignedIn();
      allow write: if isSignedIn()
                   && isOwner(userId)
                   && isImage()
                   && isSizeValid(10 * 1024 * 1024);  // 10MB
    }
    
    // === MÉTADONNÉES ===
    
    // Vérifier métadonnées custom
    match /uploads/{fileId} {
      allow write: if request.auth != null
                   && request.resource.metadata.uploadedBy == request.auth.uid;
    }
  }
}

// Déployer règles
firebase deploy --only storage


[OK] CLOUD FUNCTIONS (CODE BACKEND)


# === QU'EST-CE QUE CLOUD FUNCTIONS? ===

Exécuter code backend sans gérer de serveurs (serverless)

# Déclencheurs:
- HTTP (API REST)
- Firestore (création/modification/suppression document)
- Authentication (nouvel utilisateur)
- Storage (upload fichier)
- Pub/Sub (messages)
- Scheduled (cron jobs)

# === INITIALISER FUNCTIONS ===

# Via CLI
firebase init functions

# Choisir langage:
# - JavaScript
# - TypeScript
# - Python (Beta)

# Structure créée:
functions/
├── package.json
├── index.js ou index.ts
├── .eslintrc.js
└── node_modules/

# === INSTALLER DÉPENDANCES ===

cd functions
npm install

# Dépendances utiles:
npm install firebase-admin
npm install firebase-functions@latest
npm install axios
npm install nodemailer

# === FONCTIONS HTTP ===

// functions/index.js
const {onRequest} = require('firebase-functions/v2/https');
const {setGlobalOptions} = require('firebase-functions/v2');

// Configuration globale
setGlobalOptions({
  region: 'europe-west1'
});

// Fonction HTTP simple
exports.helloWorld = onRequest((request, response) => {
  response.send('Hello from Firebase!');
});

// Fonction avec paramètres
exports.greet = onRequest((request, response) => {
  const name = request.query.name || 'anonyme';
  response.json({
    message: `Bonjour ${name}!`,
    timestamp: new Date().toISOString()
  });
});

// URL: https://REGION-PROJECT_ID.cloudfunctions.net/greet?name=Jean

// Fonction POST
exports.createUser = onRequest(async (request, response) => {
  if (request.method !== 'POST') {
    response.status(405).send('Method Not Allowed');
    return;
  }
  
  const {name, email} = request.body;
  
  if (!name || !email) {
    response.status(400).json({error: 'Données manquantes'});
    return;
  }
  
  // Logique métier
  try {
    // Sauvegarder dans Firestore
    const admin = require('firebase-admin');
    const db = admin.firestore();
    
    const docRef = await db.collection('users').add({
      name: name,
      email: email,
      createdAt: admin.firestore.FieldValue.serverTimestamp()
    });
    
    response.json({
      success: true,
      userId: docRef.id
    });
  } catch (error) {
    console.error('Erreur:', error);
    response.status(500).json({error: 'Erreur serveur'});
  }
});

// CORS (autoriser requêtes cross-origin)
exports.api = onRequest({cors: true}, (request, response) => {
  response.json({data: 'API accessible'});
});

// Ou manuellement
const cors = require('cors')({origin: true});

exports.api = onRequest((request, response) => {
  cors(request, response, () => {
    // Logique
    response.json({data: 'API'});
  });
});

# === DÉCLENCHEURS FIRESTORE ===

const {onDocumentCreated, onDocumentUpdated, onDocumentDeleted} = 
  require('firebase-functions/v2/firestore');

// Nouveau document créé
exports.onUserCreated = onDocumentCreated('users/{userId}', (event) => {
  const snapshot = event.data;
  const data = snapshot.data();
  
  console.log('Nouvel utilisateur:', data.name);
  console.log('ID:', event.params.userId);
  
  // Envoyer email de bienvenue
  // Créer document dans autre collection
  // etc.
});

// Document modifié
exports.onUserUpdated = onDocumentUpdated('users/{userId}', (event) => {
  const before = event.data.before.data();  // Anciennes données
  const after = event.data.after.data();    // Nouvelles données
  
  console.log('Avant:', before);
  console.log('Après:', after);
  
  // Détecter changements spécifiques
  if (before.email !== after.email) {
    console.log('Email changé');
  }
});

// Document supprimé
exports.onUserDeleted = onDocumentDeleted('users/{userId}', (event) => {
  const data = event.data.data();
  
  console.log('Utilisateur supprimé:', data.name);
  
  // Nettoyer données associées
  // Supprimer fichiers Storage
  // etc.
});

// Exemple: Compter documents
exports.countPosts = onDocumentCreated('posts/{postId}', async (event) => {
  const admin = require('firebase-admin');
  const db = admin.firestore();
  
  // Incrémenter compteur
  await db.collection('stats').doc('posts').set({
    count: admin.firestore.FieldValue.increment(1)
  }, {merge: true});
});

// Exemple: Synchroniser données
exports.syncUserData = onDocumentUpdated('users/{userId}', async (event) => {
  const admin = require('firebase-admin');
  const db = admin.firestore();
  
  const userId = event.params.userId;
  const newName = event.data.after.data().name;
  
  // Mettre à jour nom dans tous les posts de l'utilisateur
  const postsSnapshot = await db.collection('posts')
    .where('authorId', '==', userId)
    .get();
  
  const batch = db.batch();
  postsSnapshot.forEach((doc) => {
    batch.update(doc.ref, {authorName: newName});
  });
  
  await batch.commit();
  console.log('Posts mis à jour');
});

# === DÉCLENCHEURS AUTHENTICATION ===

const {onUserCreated: onAuthUserCreated, onUserDeleted: onAuthUserDeleted} = 
  require('firebase-functions/v2/identity');

// Nouvel utilisateur inscrit
exports.onNewUser = onAuthUserCreated((event) => {
  const user = event.data;
  
  console.log('Nouvel utilisateur Auth:', user.uid);
  console.log('Email:', user.email);
  console.log('Provider:', user.providerData[0].providerId);
  
  // Créer document Firestore pour utilisateur
  const admin = require('firebase-admin');
  const db = admin.firestore();
  
  return db.collection('users').doc(user.uid).set({
    email: user.email,
    displayName: user.displayName || null,
    photoURL: user.photoURL || null,
    createdAt: admin.firestore.FieldValue.serverTimestamp(),
    role: 'user'
  });
});

// Utilisateur supprimé
exports.onUserDeletedAuth = onAuthUserDeleted(async (event) => {
  const user = event.data;
  
  console.log('Utilisateur supprimé:', user.uid);
  
  // Nettoyer données
  const admin = require('firebase-admin');
  const db = admin.firestore();
  const storage = admin.storage();
  
  // Supprimer document Firestore
  await db.collection('users').doc(user.uid).delete();
  
  // Supprimer fichiers Storage
  const bucket = storage.bucket();
  await bucket.deleteFiles({
    prefix: `users/${user.uid}/`
  });
  
  console.log('Données nettoyées');
});

# === DÉCLENCHEURS STORAGE ===

const {onObjectFinalized, onObjectDeleted} = 
  require('firebase-functions/v2/storage');

// Fichier uploadé
exports.onFileUpload = onObjectFinalized(async (event) => {
  const filePath = event.data.name;  // Ex: users/user123/photo.jpg
  const contentType = event.data.contentType;
  
  console.log('Fichier uploadé:', filePath);
  console.log('Type:', contentType);
  
  // Exemple: Générer miniature si image
  if (contentType && contentType.startsWith('image/')) {
    // Logique génération miniature
    console.log('Générer miniature pour', filePath);
  }
});

// Fichier supprimé
exports.onFileDelete = onObjectDeleted((event) => {
  const filePath = event.data.name;
  console.log('Fichier supprimé:', filePath);
});

// Exemple: Redimensionner image
exports.generateThumbnail = onObjectFinalized(async (event) => {
  const admin = require('firebase-admin');
  const path = require('path');
  const os = require('os');
  const fs = require('fs');
  const sharp = require('sharp');  // npm install sharp
  
  const filePath = event.data.name;
  const contentType = event.data.contentType;
  
  // Ignorer si pas image ou déjà miniature
  if (!contentType.startsWith('image/') || filePath.includes('thumb_')) {
    return null;
  }
  
  const bucket = admin.storage().bucket();
  const fileName = path.basename(filePath);
  const tempFilePath = path.join(os.tmpdir(), fileName);
  const thumbFileName = `thumb_${fileName}`;
  const thumbFilePath = path.join(os.tmpdir(), thumbFileName);
  
  // Télécharger fichier
  await bucket.file(filePath).download({destination: tempFilePath});
  
  // Redimensionner
  await sharp(tempFilePath)
    .resize(200, 200, {fit: 'inside'})
    .toFile(thumbFilePath);
  
  // Uploader miniature
  const thumbPath = path.join(path.dirname(filePath), thumbFileName);
  await bucket.upload(thumbFilePath, {
    destination: thumbPath,
    metadata: {contentType: contentType}
  });
  
  // Nettoyer fichiers temporaires
  fs.unlinkSync(tempFilePath);
  fs.unlinkSync(thumbFilePath);
  
  console.log('Miniature créée:', thumbPath);
});

# === FONCTIONS SCHEDULÉES (CRON) ===

const {onSchedule} = require('firebase-functions/v2/scheduler');

// Exécuter tous les jours à minuit
exports.dailyCleanup = onSchedule('0 0 * * *', async (event) => {
  console.log('Nettoyage quotidien');
  
  const admin = require('firebase-admin');
  const db = admin.firestore();
  
  // Supprimer vieux documents (30 jours)
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
  
  const oldDocs = await db.collection('logs')
    .where('createdAt', '<', thirtyDaysAgo)
    .get();
  
  const batch = db.batch();
  oldDocs.forEach((doc) => {
    batch.delete(doc.ref);
  });
  
  await batch.commit();
  console.log(`${oldDocs.size} documents supprimés`);
});

// Syntaxe CRON:
// ┌───────────── minute (0 - 59)
// │ ┌───────────── heure (0 - 23)
// │ │ ┌───────────── jour du mois (1 - 31)
// │ │ │ ┌───────────── mois (1 - 12)
// │ │ │ │ ┌───────────── jour de la semaine (0 - 6) (Dimanche=0)
// │ │ │ │ │
// * * * * *

// Exemples:
// '0 0 * * *'      - Tous les jours à minuit
// '0 9 * * 1'      - Tous les lundis à 9h
// '*/15 * * * *'   - Toutes les 15 minutes
// '0 12 1 * *'     - Le 1er de chaque mois à midi
// '0 0 * * 0'      - Tous les dimanches à minuit

// Toutes les heures
exports.hourlyStats = onSchedule('0 * * * *', async (event) => {
  console.log('Statistiques horaires');
  // Calculer et sauvegarder stats
});

// Avec timezone
exports.scheduledTask = onSchedule({
  schedule: '0 9 * * *',
  timeZone: 'Europe/Paris'
}, async (event) => {
  console.log('Tâche à 9h heure de Paris');
});

# === ADMIN SDK ===

// Initialiser Admin SDK (dans functions)
const admin = require('firebase-admin');
admin.initializeApp();

// Obtenir services
const db = admin.firestore();
const auth = admin.auth();
const storage = admin.storage();

// === FIRESTORE ADMIN ===

// Écrire
await db.collection('users').doc('user123').set({
  name: 'Jean',
  email: 'jean@example.com'
});

// Lire
const doc = await db.collection('users').doc('user123').get();
console.log(doc.data());

// Requête
const snapshot = await db.collection('users')
  .where('age', '>=', 18)
  .get();

snapshot.forEach((doc) => {
  console.log(doc.id, doc.data());
});

// Timestamp serveur
await db.collection('posts').add({
  title: 'Article',
  createdAt: admin.firestore.FieldValue.serverTimestamp()
});

// === AUTHENTICATION ADMIN ===

// Créer utilisateur
const userRecord = await admin.auth().createUser({
  email: 'new@example.com',
  password: 'password123',
  displayName: 'Nouvel Utilisateur',
  emailVerified: false
});

console.log('Utilisateur créé:', userRecord.uid);

// Obtenir utilisateur
const user = await admin.auth().getUser(userId);
console.log(user.email);

// Par email
const userByEmail = await admin.auth().getUserByEmail('user@example.com');

// Mettre à jour
await admin.auth().updateUser(userId, {
  displayName: 'Nouveau Nom',
  photoURL: 'https://...'
});

// Supprimer
await admin.auth().deleteUser(userId);

// Lister utilisateurs
const listUsers = await admin.auth().listUsers(1000);
listUsers.users.forEach((user) => {
  console.log(user.uid, user.email);
});

// Définir custom claims (rôles)
await admin.auth().setCustomUserClaims(userId, {
  admin: true,
  role: 'moderator'
});

// Vérifier token
const decodedToken = await admin.auth().verifyIdToken(idToken);
console.log('UID:', decodedToken.uid);
console.log('Claims:', decodedToken.admin);

// === STORAGE ADMIN ===

const bucket = admin.storage().bucket();

// Uploader
await bucket.upload('local-file.jpg', {
  destination: 'uploads/file.jpg'
});

// Télécharger
await bucket.file('uploads/file.jpg').download({
  destination: './downloaded.jpg'
});

// Supprimer
await bucket.file('uploads/file.jpg').delete();

// Obtenir URL signée (temporaire)
const [url] = await bucket.file('private/file.pdf').getSignedUrl({
  action: 'read',
  expires: Date.now() + 15 * 60 * 1000  // 15 minutes
});

console.log('URL temporaire:', url);

// Lister fichiers
const [files] = await bucket.getFiles({prefix: 'uploads/'});
files.forEach((file) => {
  console.log(file.name);
});

# === DÉPLOYER FUNCTIONS ===

# Déployer toutes les functions
firebase deploy --only functions

# Déployer function spécifique
firebase deploy --only functions:helloWorld
firebase deploy --only functions:onUserCreated

# Déployer plusieurs functions
firebase deploy --only functions:api,functions:processData

# Supprimer function
firebase functions:delete functionName

# Logs en temps réel
firebase functions:log --only helloWorld

# === VARIABLES D'ENVIRONNEMENT ===

# Définir variable
firebase functions:config:set api.key="abc123" api.url="https://api.example.com"

# Lire dans code
const functions = require('firebase-functions');
const apiKey = functions.config().api.key;
const apiUrl = functions.config().api.url;

# Lister variables
firebase functions:config:get

# Supprimer variable
firebase functions:config:unset api.key

# Développement local (.env)
# functions/.env
API_KEY=abc123
API_URL=https://api.example.com

# Lire avec dotenv
require('dotenv').config();
const apiKey = process.env.API_KEY;

# === TESTER LOCALEMENT ===

# Installer émulateurs
firebase init emulators

# Lancer émulateurs
firebase emulators:start

# Émulateurs disponibles:
# - Authentication: localhost:9099
# - Firestore: localhost:8080
# - Functions: localhost:5001
# - Storage: localhost:9199
# - Hosting: localhost:5000

# Émulateurs spécifiques
firebase emulators:start --only functions
firebase emulators:start --only firestore,functions

# UI émulateurs
# http://localhost:4000

# === EXEMPLES FUNCTIONS COMPLÈTES ===

// API REST complète
exports.api = onRequest({cors: true}, async (request, response) => {
  const admin = require('firebase-admin');
  const db = admin.firestore();
  
  // Vérifier authentification
  const token = request.headers.authorization?.split('Bearer ')[1];
  
  if (!token) {
    response.status(401).json({error: 'Non authentifié'});
    return;
  }
  
  try {
    const decodedToken = await admin.auth().verifyIdToken(token);
    const userId = decodedToken.uid;
    
    // Router
    switch (request.method) {
      case 'GET':
        // Lire données utilisateur
        const userDoc = await db.collection('users').doc(userId).get();
        response.json(userDoc.data());
        break;
        
      case 'POST':
        // Créer ressource
        const data = request.body;
        const docRef = await db.collection('posts').add({
          ...data,
          authorId: userId,
          createdAt: admin.firestore.FieldValue.serverTimestamp()
        });
        response.json({id: docRef.id});
        break;
        
      case 'PUT':
        // Mettre à jour
        await db.collection('users').doc(userId).update(request.body);
        response.json({success: true});
        break;
        
      case 'DELETE':
        // Supprimer
        const postId = request.query.id;
        await db.collection('posts').doc(postId).delete();
        response.json({success: true});
        break;
        
      default:
        response.status(405).json({error: 'Method not allowed'});
    }
  } catch (error) {
    console.error('Erreur:', error);
    response.status(500).json({error: error.message});
  }
});

// Envoi email avec Nodemailer
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  service: 'gmail',
  auth: {
    user: functions.config().email.user,
    pass: functions.config().email.password
  }
});

exports.sendWelcomeEmail = onAuthUserCreated(async (event) => {
  const user = event.data;
  
  const mailOptions = {
    from: 'noreply@myapp.com',
    to: user.email,
    subject: 'Bienvenue!',
    html: `
      <h1>Bienvenue ${user.displayName || 'nouvel utilisateur'}!</h1>
      <p>Merci de vous être inscrit.</p>
    `
  };
  
  try {
    await transporter.sendMail(mailOptions);
    console.log('Email envoyé à', user.email);
  } catch (error) {
    console.error('Erreur envoi email:', error);
  }
});

// Appel API externe
const axios = require('axios');

exports.fetchExternalData = onRequest(async (request, response) => {
  try {
    const result = await axios.get('https://api.example.com/data');
    
    // Sauvegarder dans Firestore
    const admin = require('firebase-admin');
    const db = admin.firestore();
    
    await db.collection('external_data').add({
      data: result.data,
      fetchedAt: admin.firestore.FieldValue.serverTimestamp()
    });
    
    response.json(result.data);
  } catch (error) {
    console.error('Erreur:', error);
    response.status(500).json({error: error.message});
  }
});


[OK] FIREBASE HOSTING


# === QU'EST-CE QUE HOSTING? ===

Service d'hébergement web avec:
[OK] CDN global rapide
[OK] SSL gratuit automatique
[OK] Déploiements instantanés
[OK] Historique versions
[OK] Domaines personnalisés

# === INITIALISER HOSTING ===

firebase init hosting

# Questions:
# ? What do you want to use as your public directory? public
# ? Configure as a single-page app (rewrite all urls to /index.html)? Yes
# ? Set up automatic builds and deploys with GitHub? No

# Structure:
public/
├── index.html
├── 404.html
├── css/
├── js/
└── images/

# === FICHIERS HTML ===

<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Mon App Firebase</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <h1>Bienvenue sur Firebase!</h1>
  
  <script type="module">
    import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.7.1/firebase-app.js';
    
    const firebaseConfig = { /* config */ };
    const app = initializeApp(firebaseConfig);
  </script>
  <script src="js/app.js"></script>
</body>
</html>

<!-- public/404.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Page non trouvée</title>
</head>
<body>
  <h1>404 - Page non trouvée</h1>
  <a href="/">Retour à l'accueil</a>
</body>
</html>

# === CONFIGURATION HOSTING ===

// firebase.json
{
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    
    // SPA - rediriger vers index.html
    "rewrites": [{
      "source": "**",
      "destination": "/index.html"
    }],
    
    // Redirections
    "redirects": [
      {
        "source": "/old-page",
        "destination": "/new-page",
        "type": 301
      },
      {
        "source": "/blog/:post",
        "destination": "/articles/:post",
        "type": 301
      }
    ],
    
    // Headers personnalisés
    "headers": [
      {
        "source": "**/*.@(jpg|jpeg|gif|png)",
        "headers": [{
          "key": "Cache-Control",
          "value": "max-age=7200"
        }]
      },
      {
        "source": "**/*.@(js|css)",
        "headers": [{
          "key": "Cache-Control",
          "value": "max-age=604800"
        }]
      }
    ],
    
    // Pages d'erreur personnalisées
    "cleanUrls": true,  // /about au lieu de /about.html
    "trailingSlash": false
  }
}

# === DÉPLOYER SITE ===

# Déployer
firebase deploy --only hosting

# Aperçu avant déploiement
firebase hosting:channel:deploy preview

# URL temporaire générée:
# https://PROJECT-ID--preview-RANDOM.web.app

# Tester localement
firebase serve
firebase serve --only hosting

# URL locale:
# http://localhost:5000

# === DOMAINE PERSONNALISÉ ===

# Via Console Firebase:
# 1. Hosting > Ajouter domaine personnalisé
# 2. Entrer domaine: exemple.com
# 3. Vérifier propriété domaine
# 4. Configurer DNS (A ou CNAME)
# 5. Attendre propagation DNS (quelques heures)

# Via CLI:
firebase hosting:sites:list
firebase hosting:sites:create my-site

# DNS à configurer:
# Type A:
# @ (ou domaine) -> 151.101.1.195
# @ (ou domaine) -> 151.101.65.195

# Type CNAME:
# www -> PROJECT-ID.web.app

# === GESTION VERSIONS ===

# Lister déploiements
firebase hosting:clone --help

# Voir historique
# Console Firebase > Hosting > Versions

# Rollback version précédente
# Console > Cliquer sur version > Promouvoir

# === INTÉGRATION CI/CD ===

# GitHub Actions
# .github/workflows/firebase-hosting.yml
name: Deploy to Firebase Hosting

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Install dependencies
        run: npm install
      
      - name: Build
        run: npm run build
      
      - name: Deploy to Firebase
        uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}'
          channelId: live
          projectId: PROJECT-ID

# === FRAMEWORKS WEB ===

# React
npx create-react-app my-app
cd my-app
npm run build
firebase init hosting
# public directory: build
firebase deploy

# Vue.js
npm create vue@latest
cd my-app
npm run build
firebase init hosting
# public directory: dist
firebase deploy

# Angular
ng new my-app
cd my-app
ng build
firebase init hosting
# public directory: dist/my-app
firebase deploy

# Next.js (SSR)
# Utiliser Firebase Functions pour SSR
# Ou export statique:
npm run build && npm run export
# public directory: out


[OK] FIREBASE CLI - COMMANDES ESSENTIELLES


# === AUTHENTIFICATION ===

firebase login                      # Se connecter
firebase login --reauth            # Forcer reconnexion
firebase login:list                # Lister comptes
firebase logout                    # Se déconnecter
firebase login:ci                  # Token CI/CD

# === PROJETS ===

firebase projects:list             # Lister projets
firebase projects:create           # Créer projet
firebase use PROJECT_ID            # Sélectionner projet
firebase use --add                 # Ajouter alias
firebase use production            # Utiliser alias

# Aliases (fichier .firebaserc)
{
  "projects": {
    "default": "myapp-dev",
    "production": "myapp-prod",
    "staging": "myapp-staging"
  }
}

# === INITIALISATION ===

firebase init                      # Init interactif
firebase init firestore           # Init Firestore seul
firebase init hosting             # Init Hosting seul
firebase init functions           # Init Functions seul
firebase init storage             # Init Storage seul
firebase init emulators           # Init émulateurs

# === DÉPLOIEMENT ===

firebase deploy                    # Tout déployer
firebase deploy --only firestore  # Firestore seul
firebase deploy --only hosting    # Hosting seul
firebase deploy --only functions  # Functions seul
firebase deploy --only storage    # Storage seul

# Déployer fonctions spécifiques
firebase deploy --only functions:func1,functions:func2

# Déployer règles seules
firebase deploy --only firestore:rules
firebase deploy --only storage:rules

# Déployer projet spécifique
firebase deploy -P production

# === ÉMULATEURS ===

firebase emulators:start           # Lancer tous
firebase emulators:start --only firestore,functions
firebase emulators:start --import=./data  # Importer données
firebase emulators:export ./data   # Exporter données

# UI émulateurs: http://localhost:4000

# === FIRESTORE ===

# Exporter données
firebase firestore:export gs://BUCKET_NAME/backup

# Importer données
firebase firestore:import gs://BUCKET_NAME/backup

# Supprimer collection (attention!)
firebase firestore:delete --all-collections
firebase firestore:delete "users" --recursive

# Indexes
firebase firestore:indexes        # Lister indexes
firebase deploy --only firestore:indexes

# === FUNCTIONS ===

firebase functions:list           # Lister functions
firebase functions:log            # Voir logs
firebase functions:log --only functionName
firebase functions:delete functionName

# Configuration
firebase functions:config:set key="value"
firebase functions:config:get
firebase functions:config:unset key
firebase functions:config:clone --from=prod

# === HOSTING ===

firebase hosting:channel:create preview    # Canal preview
firebase hosting:channel:deploy preview    # Déployer sur canal
firebase hosting:channel:list              # Lister canaux
firebase hosting:channel:delete preview    # Supprimer canal

firebase serve                    # Tester localement
firebase serve --only hosting,functions

# === APPLICATIONS ===

firebase apps:list                # Lister apps
firebase apps:create web my-web-app
firebase apps:create ios com.example.app
firebase apps:create android com.example.app

firebase apps:sdkconfig web       # Config SDK

# === EXTENSIONS ===

firebase ext:list                 # Extensions disponibles
firebase ext:install EXTENSION_NAME
firebase ext:configure INSTANCE_ID
firebase ext:uninstall INSTANCE_ID

# === LOGS & DEBUG ===

firebase functions:log            # Logs functions
firebase functions:log --only functionName --lines 100

# Mode debug
firebase deploy --debug
firebase emulators:start --debug

# === HELP ===

firebase --help                   # Aide générale
firebase <command> --help         # Aide commande


[OK] BONNES PRATIQUES


# === STRUCTURE PROJET ===

my-firebase-app/
├── .firebaserc             # Configuration projets
├── firebase.json           # Configuration services
├── firestore.rules         # Règles Firestore
├── firestore.indexes.json  # Indexes Firestore
├── storage.rules           # Règles Storage
├── functions/              # Cloud Functions
│   ├── src/
│   │   ├── index.ts
│   │   ├── auth.ts
│   │   ├── firestore.ts
│   │   └── utils.ts
│   ├── package.json
│   └── tsconfig.json
├── public/                 # Hosting
│   ├── index.html
│   ├── css/
│   ├── js/
│   └── assets/
├── src/                    # Code application
│   ├── config/
│   │   └── firebase.js     # Configuration Firebase
│   ├── services/
│   │   ├── auth.service.js
│   │   ├── firestore.service.js
│   │   └── storage.service.js
│   ├── components/
│   └── utils/
├── .env                    # Variables environnement
├── .gitignore
├── package.json
└── README.md

# === SÉCURITÉ ===

# 1. TOUJOURS utiliser règles de sécurité
# Firestore et Storage doivent avoir règles strictes

# 2. Ne JAMAIS commiter clés API privées
# .env dans .gitignore

# 3. Valider données côté serveur
# Utiliser Functions pour validation importante

# 4. Limiter taille/type fichiers Storage
# Dans storage.rules

# 5. Utiliser HTTPS uniquement
# Firebase force HTTPS automatiquement

# 6. Activer App Check (bot protection)
# Console Firebase > App Check

# 7. Audit régulier règles sécurité
# Rules playground dans Console

# === PERFORMANCE ===

# 1. Indexer requêtes Firestore
# Créer indexes composites pour requêtes complexes

# 2. Limiter lectures Firestore
# Utiliser pagination, limit()

# 3. Cache côté client
# enableIndexedDbPersistence() pour Firestore

# 4. Optimiser images Storage
# Redimensionner avant upload
# Utiliser formats modernes (WebP)

# 5. CDN Hosting
# Utiliser Cache-Control headers

# 6. Batch writes Firestore
# Grouper écritures (max 500)

# 7. Minimiser listeners temps réel
# Désabonner quand non nécessaire

# === COÛTS ===

# Firestore:
# - Reads: $0.06 par 100k
# - Writes: $0.18 par 100k
# - Deletes: $0.02 par 100k
# - Storage: $0.18/GB/mois

# Storage:
# - Storage: $0.026/GB/mois
# - Download: $0.12/GB
# - Upload: Gratuit

# Functions:
# - Invocations: Gratuit 2M/mois
# - Compute time: Variable selon ressources

# Hosting:
# - Storage: $0.026/GB
# - Bandwidth: $0.15/GB (gratuit 10GB/mois)

# Plan Spark (Gratuit):
# - Firestore: 50k reads, 20k writes/jour
# - Storage: 1GB
# - Functions: 125k invocations/mois
# - Hosting: 10GB bandwidth/mois

# Plan Blaze (Pay-as-you-go):
# - Inclut quotas gratuits
# - Facturé au-delà

# Optimiser coûts:
# - Cache côté client
# - Pagination
# - Batch operations
# - Règles sécurité strictes (éviter lectures inutiles)

# === ORGANISATION CODE ===

// config/firebase.js - Centraliser configuration
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getFirestore } from 'firebase/firestore';
import { getStorage } from 'firebase/storage';

const firebaseConfig = {
  // Configuration
};

const app = initializeApp(firebaseConfig);

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

// services/auth.service.js - Service auth
import { auth } from '../config/firebase';
import { 
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut 
} from 'firebase/auth';

export const authService = {
  async register(email, password) {
    return await createUserWithEmailAndPassword(auth, email, password);
  },
  
  async login(email, password) {
    return await signInWithEmailAndPassword(auth, email, password);
  },
  
  async logout() {
    return await signOut(auth);
  },
  
  getCurrentUser() {
    return auth.currentUser;
  }
};

// services/firestore.service.js - Service Firestore
import { db } from '../config/firebase';
import {
  collection,
  doc,
  addDoc,
  getDoc,
  getDocs,
  updateDoc,
  deleteDoc,
  query,
  where
} from 'firebase/firestore';

export const firestoreService = {
  // Users
  async createUser(userId, data) {
    return await setDoc(doc(db, 'users', userId), data);
  },
  
  async getUser(userId) {
    const docSnap = await getDoc(doc(db, 'users', userId));
    return docSnap.exists() ? docSnap.data() : null;
  },
  
  async updateUser(userId, data) {
    return await updateDoc(doc(db, 'users', userId), data);
  },
  
  // Posts
  async createPost(data) {
    return await addDoc(collection(db, 'posts'), data);
  },
  
  async getPosts(limit = 10) {
    const q = query(collection(db, 'posts'), orderBy('createdAt', 'desc'), limit(limit));
    const snapshot = await getDocs(q);
    return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  }
};

# === ENVIRONNEMENTS (DEV/PROD) ===

# Fichiers .env
# .env.development
VITE_FIREBASE_API_KEY=dev-key
VITE_FIREBASE_PROJECT_ID=myapp-dev

# .env.production
VITE_FIREBASE_API_KEY=prod-key
VITE_FIREBASE_PROJECT_ID=myapp-prod

# Configuration dynamique
const firebaseConfig = {
  apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
  projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
  // ...
};

# Projets Firebase séparés
# - myapp-dev (développement)
# - myapp-staging (pré-production)
# - myapp-prod (production)

# .firebaserc
{
  "projects": {
    "default": "myapp-dev",
    "staging": "myapp-staging",
    "production": "myapp-prod"
  }
}

# Déployer selon environnement
firebase use staging
firebase deploy

firebase use production
firebase deploy


[OK] DÉPANNAGE & ERREURS COURANTES


# === ERREURS AUTHENTICATION ===

# auth/invalid-api-key
# Solution: Vérifier apiKey dans firebaseConfig

# auth/app-deleted
# Solution: Réinitialiser Firebase app

# auth/user-token-expired
# Solution: Redemander connexion utilisateur

# auth/network-request-failed
# Solution: Vérifier connexion internet

# auth/too-many-requests
# Solution: Attendre ou réinitialiser IP

# auth/web-storage-unsupported
# Solution: Vérifier localStorage activé dans navigateur

# auth/popup-blocked
# Solution: Autoriser popups ou utiliser redirect

# auth/unauthorized-domain
# Solution: Ajouter domaine dans Console Firebase
# Authentication > Settings > Authorized domains

# === ERREURS FIRESTORE ===

# permission-denied
# Solution: Vérifier règles de sécurité
# Tester dans Rules Playground

# failed-precondition: The query requires an index
# Solution: Créer index via lien dans erreur
# Ou dans firestore.indexes.json

# Exemple erreur:
# "The query requires an index. You can create it here: 
# https://console.firebase.google.com/..."
# Cliquer lien -> Index créé automatiquement

# deadline-exceeded
# Solution: Optimiser requête, réduire limite

# unavailable
# Solution: Réessayer avec backoff exponentiel

# not-found
# Solution: Vérifier que document/collection existe

# already-exists
# Solution: Utiliser update() au lieu de set()

# === ERREURS STORAGE ===

# storage/unauthorized
# Solution: Vérifier règles storage.rules

# storage/retry-limit-exceeded
# Solution: Vérifier connexion, réessayer

# storage/invalid-checksum
# Solution: Fichier corrompu, re-uploader

# storage/canceled
# Solution: Upload annulé par utilisateur

# storage/quota-exceeded
# Solution: Augmenter quota ou nettoyer fichiers

# === ERREURS FUNCTIONS ===

# Function execution took 60000 ms, finished with status: 'timeout'
# Solution: Augmenter timeout ou optimiser code

# functions:
#   functionName:
#     timeoutSeconds: 300
#     memory: 512MB

# Error: memory limit exceeded
# Solution: Augmenter mémoire allouée

# Error: CORS
# Solution: Activer CORS dans function

const cors = require('cors')({origin: true});

exports.api = onRequest((req, res) => {
  cors(req, res, () => {
    // Code
  });
});

# Module not found
# Solution: Installer dépendances
cd functions
npm install

# === ERREURS DÉPLOIEMENT ===

# HTTP Error: 403, The caller does not have permission
# Solution: 
firebase login --reauth
firebase use --add

# Billing account required
# Solution: Activer plan Blaze si nécessaire

# Functions deployment failed
# Solution: Vérifier logs
firebase functions:log --only functionName

# === ÉMULATEURS ===

# Port already in use
# Solution: Changer ports dans firebase.json

{
  "emulators": {
    "auth": {"port": 9099},
    "firestore": {"port": 8080},
    "functions": {"port": 5001}
  }
}

# Ou arrêter processus:
lsof -ti:5001 | xargs kill -9    # Mac/Linux
netstat -ano | findstr :5001     # Windows

# === DEBUGGING ===

// Activer logs Firestore
import { enableLogging } from 'firebase/firestore';
enableLogging(true);

// Logs détaillés Functions
console.log('Debug:', JSON.stringify(data, null, 2));

// Vérifier configuration
console.log('Firebase config:', app.options);
console.log('Auth initialized:', auth.app.name);
console.log('Current user:', auth.currentUser);

# === PROBLÈMES COURANTS ===

# Firestore en mode offline
# Symptôme: Opérations échouent sans internet
# Solution: Gérer état offline

import { onSnapshot } from 'firebase/firestore';

onSnapshot(docRef, 
  (doc) => {
    if (doc.metadata.fromCache) {
      console.log('Données du cache (offline)');
    } else {
      console.log('Données du serveur (online)');
    }
  }
);

# Listeners non désabonnés
# Symptôme: Memory leaks, données dupliquées
# Solution: Toujours unsubscribe

useEffect(() => {
  const unsubscribe = onSnapshot(query, (snapshot) => {
    // Traiter données
  });
  
  return () => unsubscribe();  // Cleanup
}, []);

# Règles trop permissives
# Symptôme: Avertissements sécurité
# Solution: Règles strictes en production

// [X] Mode test (dangereux en prod)
allow read, write: if true;

// [OK] Mode production
allow read: if request.auth != null;
allow write: if request.auth != null && request.auth.uid == userId;

# Coûts inattendus
# Symptôme: Facture élevée
# Solution: Monitorer usage

# Console Firebase > Usage and billing
# Activer alertes budget
# Analyser requêtes dans Firebase Console

# Optimisations:
# - Pagination systématique
# - Cache côté client
# - Désabonner listeners inutilisés
# - Batch operations


[OK] EXEMPLES APPLICATIONS COMPLÈTES


# === CHAT EN TEMPS RÉEL ===

// Structure Firestore
chatrooms (collection)
  └── room123 (document)
      ├── name: "Salle générale"
      ├── createdAt: timestamp
      └── messages (sous-collection)
          └── msg456 (document)
              ├── text: "Bonjour!"
              ├── userId: "user123"
              ├── userName: "Jean"
              ├── createdAt: timestamp

// Envoyer message
async function sendMessage(roomId, text) {
  const user = auth.currentUser;
  
  if (!user) {
    throw new Error('Non connecté');
  }
  
  await addDoc(collection(db, 'chatrooms', roomId, 'messages'), {
    text: text,
    userId: user.uid,
    userName: user.displayName || 'Anonyme',
    createdAt: serverTimestamp()
  });
}

// Écouter messages en temps réel
function listenToMessages(roomId, callback) {
  const q = query(
    collection(db, 'chatrooms', roomId, 'messages'),
    orderBy('createdAt', 'asc'),
    limit(50)
  );
  
  return onSnapshot(q, (snapshot) => {
    const messages = [];
    snapshot.forEach((doc) => {
      messages.push({ id: doc.id, ...doc.data() });
    });
    callback(messages);
  });
}

// Utilisation React
function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);
  const [newMessage, setNewMessage] = useState('');
  
  useEffect(() => {
    const unsubscribe = listenToMessages(roomId, setMessages);
    return () => unsubscribe();
  }, [roomId]);
  
  const handleSend = async () => {
    if (newMessage.trim()) {
      await sendMessage(roomId, newMessage);
      setNewMessage('');
    }
  };
  
  return (
    <div>
      <div className="messages">
        {messages.map(msg => (
          <div key={msg.id}>
            <strong>{msg.userName}:</strong> {msg.text}
          </div>
        ))}
      </div>
      
      <input 
        value={newMessage}
        onChange={(e) => setNewMessage(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && handleSend()}
      />
      <button onClick={handleSend}>Envoyer</button>
    </div>
  );
}

# === LISTE TODO COLLABORATIVE ===

// Structure
todos (collection)
  └── todo123 (document)
      ├── title: "Acheter lait"
      ├── completed: false
      ├── userId: "user123"
      ├── createdAt: timestamp
      ├── sharedWith: ["user456", "user789"]

// Service todos
export const todoService = {
  async createTodo(title) {
    return await addDoc(collection(db, 'todos'), {
      title: title,
      completed: false,
      userId: auth.currentUser.uid,
      createdAt: serverTimestamp(),
      sharedWith: []
    });
  },
  
  async toggleTodo(todoId, completed) {
    return await updateDoc(doc(db, 'todos', todoId), {
      completed: !completed,
      updatedAt: serverTimestamp()
    });
  },
  
  async deleteTodo(todoId) {
    return await deleteDoc(doc(db, 'todos', todoId));
  },
  
  async shareTodo(todoId, userEmail) {
    // Trouver userId par email
    const usersRef = collection(db, 'users');
    const q = query(usersRef, where('email', '==', userEmail));
    const snapshot = await getDocs(q);
    
    if (snapshot.empty) {
      throw new Error('Utilisateur non trouvé');
    }
    
    const targetUserId = snapshot.docs[0].id;
    
    await updateDoc(doc(db, 'todos', todoId), {
      sharedWith: arrayUnion(targetUserId)
    });
  },
  
  listenToUserTodos(callback) {
    const userId = auth.currentUser.uid;
    
    const q = query(
      collection(db, 'todos'),
      where('userId', '==', userId),
      orderBy('createdAt', 'desc')
    );
    
    return onSnapshot(q, (snapshot) => {
      const todos = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      }));
      callback(todos);
    });
  }
};

// Règles Firestore
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /todos/{todoId} {
      allow read: if request.auth != null &&
        (resource.data.userId == request.auth.uid ||
         request.auth.uid in resource.data.sharedWith);
      
      allow create: if request.auth != null &&
        request.resource.data.userId == request.auth.uid;
      
      allow update, delete: if request.auth != null &&
        resource.data.userId == request.auth.uid;
    }
  }
}

# === GALERIE PHOTOS ===

// Upload avec preview et progression
async function uploadPhoto(file, userId) {
  // Vérifier type
  if (!file.type.startsWith('image/')) {
    throw new Error('Fichier doit être une image');
  }
  
  // Vérifier taille (max 5MB)
  if (file.size > 5 * 1024 * 1024) {
    throw new Error('Image trop grande (max 5MB)');
  }
  
  // Créer nom unique
  const timestamp = Date.now();
  const fileName = `${timestamp}-${file.name}`;
  const filePath = `users/${userId}/photos/${fileName}`;
  
  // Upload avec progression
  const storageRef = ref(storage, filePath);
  const uploadTask = uploadBytesResumable(storageRef, file);
  
  return new Promise((resolve, reject) => {
    uploadTask.on('state_changed',
      (snapshot) => {
        const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
        console.log('Upload:', progress.toFixed(0) + '%');
      },
      (error) => {
        reject(error);
      },
      async () => {
        // Succès - obtenir URL
        const downloadURL = await getDownloadURL(uploadTask.snapshot.ref);
        
        // Sauvegarder metadata dans Firestore
        const photoDoc = await addDoc(collection(db, 'photos'), {
          userId: userId,
          url: downloadURL,
          fileName: file.name,
          filePath: filePath,
          size: file.size,
          contentType: file.type,
          createdAt: serverTimestamp()
        });
        
        resolve({
          id: photoDoc.id,
          url: downloadURL
        });
      }
    );
  });
}

// Composant React galerie
function PhotoGallery() {
  const [photos, setPhotos] = useState([]);
  const [uploading, setUploading] = useState(false);
  
  useEffect(() => {
    const userId = auth.currentUser.uid;
    const q = query(
      collection(db, 'photos'),
      where('userId', '==', userId),
      orderBy('createdAt', 'desc')
    );
    
    const unsubscribe = onSnapshot(q, (snapshot) => {
      const photoList = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      }));
      setPhotos(photoList);
    });
    
    return () => unsubscribe();
  }, []);
  
  const handleUpload = async (e) => {
    const file = e.target.files[0];
    if (!file) return;
    
    setUploading(true);
    try {
      await uploadPhoto(file, auth.currentUser.uid);
    } catch (error) {
      alert('Erreur upload: ' + error.message);
    } finally {
      setUploading(false);
    }
  };
  
  const handleDelete = async (photo) => {
    if (!confirm('Supprimer cette photo?')) return;
    
    try {
      // Supprimer de Storage
      await deleteObject(ref(storage, photo.filePath));
      
      // Supprimer de Firestore
      await deleteDoc(doc(db, 'photos', photo.id));
    } catch (error) {
      alert('Erreur suppression: ' + error.message);
    }
  };
  
  return (
    <div>
      <input 
        type="file" 
        accept="image/*" 
        onChange={handleUpload}
        disabled={uploading}
      />
      
      <div className="gallery">
        {photos.map(photo => (
          <div key={photo.id} className="photo-item">
            <img src={photo.url} alt={photo.fileName} />
            <button onClick={() => handleDelete(photo)}>
              Supprimer
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

# === SYSTÈME NOTIFICATIONS ===

// Cloud Function - Notification nouvel abonné
exports.notifyNewFollower = onDocumentCreated(
  'followers/{followerId}',
  async (event) => {
    const data = event.data.data();
    const followerId = event.params.followerId;
    const followedUserId = data.followedUserId;
    
    // Obtenir info follower
    const followerDoc = await admin.firestore()
      .collection('users')
      .doc(data.followerId)
      .get();
    
    const followerName = followerDoc.data().name;
    
    // Créer notification
    await admin.firestore()
      .collection('users')
      .doc(followedUserId)
      .collection('notifications')
      .add({
        type: 'new_follower',
        message: `${followerName} vous suit maintenant`,
        followerId: data.followerId,
        read: false,
        createdAt: admin.firestore.FieldValue.serverTimestamp()
      });
    
    // Incrémenter compteur non lues
    await admin.firestore()
      .collection('users')
      .doc(followedUserId)
      .update({
        unreadNotifications: admin.firestore.FieldValue.increment(1)
      });
  }
);

// Frontend - Écouter notifications
function useNotifications() {
  const [notifications, setNotifications] = useState([]);
  const [unreadCount, setUnreadCount] = useState(0);
  
  useEffect(() => {
    if (!auth.currentUser) return;
    
    const notifRef = collection(
      db, 
      'users', 
      auth.currentUser.uid, 
      'notifications'
    );
    
    const q = query(
      notifRef,
      where('read', '==', false),
      orderBy('createdAt', 'desc'),
      limit(20)
    );
    
    const unsubscribe = onSnapshot(q, (snapshot) => {
      const notifs = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      }));
      
      setNotifications(notifs);
      setUnreadCount(notifs.length);
    });
    
    return () => unsubscribe();
  }, []);
  
  const markAsRead = async (notificationId) => {
    await updateDoc(
      doc(db, 'users', auth.currentUser.uid, 'notifications', notificationId),
      { read: true }
    );
  };
  
  const markAllAsRead = async () => {
    const batch = writeBatch(db);
    
    notifications.forEach((notif) => {
      const notifRef = doc(
        db, 
        'users', 
        auth.currentUser.uid, 
        'notifications', 
        notif.id
      );
      batch.update(notifRef, { read: true });
    });
    
    await batch.commit();
  };
  
  return { notifications, unreadCount, markAsRead, markAllAsRead };
}


[OK] INTÉGRATIONS AVANCÉES


# === STRIPE PAYMENTS ===

# Extension Firebase
firebase ext:install stripe/firestore-stripe-payments

# Configuration
# - Clés API Stripe (test/prod)
# - Collection produits: products
# - Collection clients: customers

// Créer checkout session
import { addDoc, collection } from 'firebase/firestore';

async function createCheckoutSession(priceId) {
  const userId = auth.currentUser.uid;
  
  const checkoutRef = await addDoc(
    collection(db, 'customers', userId, 'checkout_sessions'),
    {
      price: priceId,
      success_url: window.location.origin + '/success',
      cancel_url: window.location.origin + '/canceled',
      mode: 'payment'
    }
  );
  
  // Écouter session
  onSnapshot(checkoutRef, (snap) => {
    const { error, url } = snap.data();
    
    if (error) {
      alert('Erreur: ' + error.message);
    }
    
    if (url) {
      window.location.assign(url);
    }
  });
}

// Vérifier abonnement
async function checkSubscription() {
  const userId = auth.currentUser.uid;
  
  const subscriptionsRef = collection(db, 'customers', userId, 'subscriptions');
  const q = query(subscriptionsRef, where('status', 'in', ['trialing', 'active']));
  
  const snapshot = await getDocs(q);
  return !snapshot.empty;
}

# === ALGOLIA SEARCH ===

# Extension Firebase
firebase ext:install algolia/firestore-algolia-search

# Ou manuel:
npm install algoliasearch

// Configuration
import algoliasearch from 'algoliasearch';

const client = algoliasearch('APP_ID', 'SEARCH_API_KEY');
const index = client.initIndex('products');

// Rechercher
async function search(query) {
  try {
    const { hits } = await index.search(query);
    return hits;
  } catch (error) {
    console.error('Erreur recherche:', error);
    return [];
  }
}

// Cloud Function - Synchroniser avec Algolia
const algoliasearch = require('algoliasearch');

exports.syncToAlgolia = onDocumentWritten('products/{productId}', async (event) => {
  const data = event.data.after.data();
  const productId = event.params.productId;
  
  const client = algoliasearch('APP_ID', 'ADMIN_API_KEY');
  const index = client.initIndex('products');
  
  if (event.data.after.exists) {
    // Créer ou mettre à jour
    await index.saveObject({
      objectID: productId,
      ...data
    });
  } else {
    // Supprimer
    await index.deleteObject(productId);
  }
});

# === SENDGRID EMAILS ===

# Extension Firebase
firebase ext:install sendgrid/firestore-send-email

# Ou Cloud Function
const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(functions.config().sendgrid.key);

exports.sendWelcomeEmail = onAuthUserCreated(async (event) => {
  const user = event.data;
  
  const msg = {
    to: user.email,
    from: 'noreply@myapp.com',
    subject: 'Bienvenue!',
    text: 'Merci de votre inscription',
    html: '<strong>Merci de votre inscription</strong>'
  };
  
  try {
    await sgMail.send(msg);
    console.log('Email envoyé');
  } catch (error) {
    console.error('Erreur:', error);
  }
});

# === GOOGLE MAPS ===

// Stocker localisation dans Firestore
import { GeoPoint } from 'firebase/firestore';

await addDoc(collection(db, 'places'), {
  name: 'Restaurant',
  location: new GeoPoint(48.8566, 2.3522),
  address: '123 Rue de Paris'
});

// Requête par proximité (utiliser GeoFirestore)
npm install geofirestore

import { GeoFirestore } from 'geofirestore';

const geofirestore = new GeoFirestore(db);
const geocollection = geofirestore.collection('places');

// Requête dans rayon
const query = geocollection.near({
  center: new firebase.firestore.GeoPoint(48.8566, 2.3522),
  radius: 5  // km
});

const snapshot = await query.get();


[OK] MIGRATION & MAINTENANCE


# === BACKUP FIRESTORE ===

# Export manuel
firebase firestore:export gs://BUCKET_NAME/backups/$(date +%Y%m%d)

# Script backup automatique
#!/bin/bash
DATE=$(date +%Y%m%d)
firebase firestore:export gs://myapp-backups/firestore-$DATE

# Cloud Function backup schedulé
exports.backupFirestore = onSchedule('0 2 * * *', async (event) => {
  const admin = require('firebase-admin');
  const client = new admin.firestore.v1.FirestoreAdminClient();
  
  const projectId = process.env.GCLOUD_PROJECT;
  const databaseName = client.databasePath(projectId, '(default)');
  
  await client.exportDocuments({
    name: databaseName,
    outputUriPrefix: `gs://${projectId}-backups/firestore-${new Date().toISOString()}`,
    collectionIds: []  // Toutes les collections
  });
});

# Restore
firebase firestore:import gs://BUCKET_NAME/backups/20240101

# === MIGRATION DONNÉES ===

// Script migration
const admin = require('firebase-admin');
admin.initializeApp();

const db = admin.firestore();

async function migrateUsers() {
  const snapshot = await db.collection('users').get();
  
  const batch = db.batch();
  let count = 0;
  
  snapshot.forEach((doc) => {
    const data = doc.data();
    
    // Ajouter nouveau champ
    batch.update(doc.ref, {
      role: data.role || 'user',
      lastUpdated: admin.firestore.FieldValue.serverTimestamp()
    });
    
    count++;
    
    // Batch max 500 opérations
    if (count % 500 === 0) {
      batch.commit();
    }
  });
  
  await batch.commit();
  console.log(`${count} utilisateurs migrés`);
}

migrateUsers();

# === NETTOYAGE DONNÉES ===

// Supprimer vieux documents
async function cleanOldLogs() {
  const thirtyDaysAgo = new Date();
  thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
  
  const snapshot = await db.collection('logs')
    .where('createdAt', '<', thirtyDaysAgo)
    .limit(500)  // Batch par 500
    .get();
  
  if (snapshot.empty) {
    console.log('Aucun document à supprimer');
    return;
  }
  
  const batch = db.batch();
  snapshot.docs.forEach((doc) => {
    batch.delete(doc.ref);
  });
  
  await batch.commit();
  console.log(`${snapshot.size} documents supprimés`);
  
  // Récursif si plus de documents
  if (snapshot.size === 500) {
    await cleanOldLogs();
  }
}

# === MONITORING ===

// Cloud Function - Logger erreurs
exports.logError = onRequest(async (request, response) => {
  const { error, userId, context } = request.body;
  
  await admin.firestore().collection('errors').add({
    error: error,
    userId: userId || null,
    context: context || {},
    timestamp: admin.firestore.FieldValue.serverTimestamp(),
    userAgent: request.headers['user-agent']
  });
  
  response.json({ logged: true });
});

// Frontend - Envoyer erreurs
window.addEventListener('error', async (event) => {
  await fetch('https://REGION-PROJECT.cloudfunctions.net/logError', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      error: event.error.message,
      userId: auth.currentUser?.uid,
      context: {
        page: window.location.href,
        stack: event.error.stack
      }
    })
  });
});


[OK] RESSOURCES & DOCUMENTATION


# === DOCUMENTATION OFFICIELLE ===

# Firebase
https://firebase.google.com/docs

# Authentication
https://firebase.google.com/docs/auth

# Firestore
https://firebase.google.com/docs/firestore

# Cloud Storage
https://firebase.google.com/docs/storage

# Cloud Functions
https://firebase.google.com/docs/functions

# Hosting
https://firebase.google.com/docs/hosting

# Firebase CLI
https://firebase.google.com/docs/cli

# === GUIDES & TUTORIELS ===

# Getting Started
https://firebase.google.com/docs/web/setup

# Codelab officiel
https://firebase.google.com/codelabs

# YouTube Firebase
https://www.youtube.com/firebase

# Fireship.io (tutoriels vidéo)
https://fireship.io/tags/firebase/

# === OUTILS ===

# Firebase Console
https://console.firebase.google.com

# Emulator Suite
https://firebase.google.com/docs/emulator-suite

# Firebase Extensions
https://firebase.google.com/products/extensions

# Status page
https://status.firebase.google.com

# === COMMUNAUTÉ ===

# Stack Overflow
https://stackoverflow.com/questions/tagged/firebase

# GitHub Discussions
https://github.com/firebase/firebase-js-sdk/discussions

# Discord Firebase
https://discord.gg/firebase

# Reddit
https://reddit.com/r/firebase

# === EXEMPLES CODE ===

# Samples officiels
https://github.com/firebase/quickstart-js
https://github.com/firebase/friendlyeats-web
https://github.com/firebase/codelab-friendlychat-web

# Templates
https://github.com/firebase/firebase-templates

# === PRICING CALCULATOR ===

https://firebase.google.com/pricing

# === CHANGELOG ===

# SDK JavaScript
https://firebase.google.com/support/release-notes/js

# Firebase CLI
https://github.com/firebase/firebase-tools/releases


[OK] CHECKLIST DÉPLOIEMENT PRODUCTION


[WHITE_SQUARE] Sécurité
  [WHITE_SQUARE] Règles Firestore configurées et testées
  [WHITE_SQUARE] Règles Storage configurées et testées
  [WHITE_SQUARE] Authentification configurée (méthodes actives)
  [WHITE_SQUARE] App Check activé
  [WHITE_SQUARE] Variables sensibles dans Environment Config
  [WHITE_SQUARE] CORS configuré pour Functions
  
[WHITE_SQUARE] Performance
  [WHITE_SQUARE] Index Firestore créés
  [WHITE_SQUARE] Queries optimisées (limit, pagination)
  [WHITE_SQUARE] Images optimisées (compression, formats modernes)
  [WHITE_SQUARE] Cache-Control headers configurés (Hosting)
  [WHITE_SQUARE] Listeners désabonnés correctement
  
[WHITE_SQUARE] Monitoring
  [WHITE_SQUARE] Analytics activé
  [WHITE_SQUARE] Crashlytics configuré (mobile)
  [WHITE_SQUARE] Logging erreurs en place
  [WHITE_SQUARE] Alertes budget configurées
  
[WHITE_SQUARE] Backup
  [WHITE_SQUARE] Plan backup Firestore en place
  [WHITE_SQUARE] Backup Storage configuré
  [WHITE_SQUARE] Procédure restore testée
  
[WHITE_SQUARE] Testing
  [WHITE_SQUARE] Tests avec émulateurs
  [WHITE_SQUARE] Tests règles sécurité
  [WHITE_SQUARE] Tests fonctions cloud
  [WHITE_SQUARE] Tests sur environnement staging
  
[WHITE_SQUARE] Documentation
  [WHITE_SQUARE] README à jour
  [WHITE_SQUARE] Variables environnement documentées
  [WHITE_SQUARE] Procédures déploiement documentées
  [WHITE_SQUARE] Architecture documentée
  
[WHITE_SQUARE] Legal
  [WHITE_SQUARE] Politique confidentialité
  [WHITE_SQUARE] CGU/CGV
  [WHITE_SQUARE] RGPD (si Europe)
  [WHITE_SQUARE] Mentions légales


[OK] ANTI-PATTERNS À ÉVITER


# [X] Règles trop permissives en production
allow read, write: if true;

# [OK] Règles strictes
allow read, write: if request.auth != null;

# [X] Pas de pagination
const snapshot = await getDocs(collection(db, 'posts'));  // Tous les docs!

# [OK] Avec limit
const q = query(collection(db, 'posts'), limit(20));

# [X] Listeners non désabonnés
onSnapshot(docRef, (doc) => {
  // Update UI
});

# [OK] Cleanup
useEffect(() => {
  const unsubscribe = onSnapshot(docRef, (doc) => {
    // Update UI
  });
  return () => unsubscribe();
}, []);

# [X] Requêtes sans index
where('status', '==', 'active').orderBy('createdAt', 'desc')
// Erreur: index requis!

# [OK] Créer index avant
firebase deploy --only firestore:indexes

# [X] Upload fichiers énormes sans validation
await uploadBytes(ref, file);  // Pas de vérification taille!

# [OK] Valider avant upload
if (file.size > 5 * 1024 * 1024) {
  throw new Error('Fichier trop grand (max 5MB)');
}

# [X] Stocker données sensibles en clair
await setDoc(doc(db, 'users', userId), {
  password: 'password123',  // JAMAIS!
  creditCard: '1234-5678-9012-3456'  // JAMAIS!
});

# [OK] Ne jamais stocker mots de passe
# Utiliser Firebase Auth
# Tokeniser paiements avec Stripe

# [X] Boucles de lectures Firestore
for (const userId of userIds) {
  const doc = await getDoc(doc(db, 'users', userId));  // N requêtes!
}

# [OK] Batch ou query
const q = query(collection(db, 'users'), where('id', 'in', userIds));
const snapshot = await getDocs(q);

# [X] Modifier même document depuis listeners
onSnapshot(docRef, async (doc) => {
  await updateDoc(docRef, { count: doc.data().count + 1 });
  // Boucle infinie!
});

# [OK] Utiliser increment
await updateDoc(docRef, { count: increment(1) });

# [X] Exposer clés API admin
const firebaseConfig = {
  apiKey: "ADMIN_KEY_SECRET",  // Ne pas confondre!
  // ...
};

# [OK] apiKey web est publique, c'est normal
# Sécurité = règles Firestore/Storage

# [X] Ignorer erreurs
try {
  await someFirebaseOperation();
} catch (error) {
  // Rien...
}

# [OK] Gérer erreurs
try {
  await someFirebaseOperation();
} catch (error) {
  console.error('Erreur Firebase:', error.code, error.message);
  // Informer utilisateur
  showErrorMessage(error.message);
}

# [X] Synchronisation massive
// Mauvais pour 10000 documents
const snapshot = await getDocs(collection(db, 'items'));
snapshot.forEach(doc => {
  await updateDoc(doc.ref, { migrated: true });
});

# [OK] Batch writes
const batch = writeBatch(db);
snapshot.forEach(doc => {
  batch.update(doc.ref, { migrated: true });
  if (batch._mutations.length === 500) {
    await batch.commit();
  }
});

# [X] Données dupliquées sans raison
// Garder copie complète de l'utilisateur partout
posts/post123: {
  author: {
    id: 'user123',
    name: 'Jean',
    email: 'jean@example.com',
    bio: '...',
    avatar: '...',
    // 50 autres champs...
  }
}

# [OK] Dénormalisation intelligente
posts/post123: {
  authorId: 'user123',
  authorName: 'Jean',
  authorAvatar: 'https://...'
  // Seulement données affichées
}


[OK] PATTERNS & ARCHITECTURE


# === STRUCTURE DONNÉES FIRESTORE ===

# Pattern: Dénormalisation
# Dupliquer données fréquemment lues ensemble

// Au lieu de:
users/{userId}
posts/{postId} -> authorId

// Faire:
users/{userId}
posts/{postId} -> {
  authorId: userId,
  authorName: 'Jean',
  authorAvatar: 'url'
}

# Trade-off: Lectures rapides vs écritures multiples

# Pattern: Collections de niveau supérieur
# Préférer collections plates aux sous-collections profondes

// [X] Trop profond
users/{userId}/posts/{postId}/comments/{commentId}/likes/{likeId}

// [OK] Aplati
posts/{postId}
comments/{commentId} -> postId
likes/{likeId} -> commentId

# Pattern: Compteurs distribués
# Pour compteurs haute fréquence

likes (collection)
  └── post123 (document)
      └── shards (sous-collection)
          ├── shard0: { count: 42 }
          ├── shard1: { count: 38 }
          └── shard2: { count: 51 }

// Function incrémenter
async function incrementLikes(postId) {
  const numShards = 10;
  const shardId = Math.floor(Math.random() * numShards);
  
  const shardRef = doc(db, 'likes', postId, 'shards', `shard${shardId}`);
  
  await setDoc(shardRef, {
    count: increment(1)
  }, { merge: true });
}

// Lire total
async function getTotalLikes(postId) {
  const shardsSnapshot = await getDocs(
    collection(db, 'likes', postId, 'shards')
  );
  
  let total = 0;
  shardsSnapshot.forEach(doc => {
    total += doc.data().count || 0;
  });
  
  return total;
}

# Pattern: Index inversé
# Pour recherches multiples

// Structure
userSkills (collection)
  └── skill_python_user_123 (document)
      ├── skillId: 'python'
      ├── userId: 'user123'
      └── level: 'expert'

// Trouver utilisateurs avec compétence
const q = query(
  collection(db, 'userSkills'),
  where('skillId', '==', 'python'),
  where('level', '==', 'expert')
);

# Pattern: Timestamps & Soft Delete
// Toujours inclure timestamps

{
  title: 'Article',
  createdAt: serverTimestamp(),
  updatedAt: serverTimestamp(),
  deletedAt: null  // Soft delete
}

// Filtrer non supprimés
where('deletedAt', '==', null)

# Pattern: Status tracking
// États explicites

{
  status: 'draft' | 'published' | 'archived',
  publishedAt: timestamp | null,
  archivedAt: timestamp | null
}

# === ARCHITECTURE APPLICATIONS ===

# Pattern: Service Layer
# Séparer logique métier de Firebase

// services/user.service.js
export const userService = {
  async createUser(userData) {
    // Validation
    if (!userData.email) throw new Error('Email requis');
    
    // Logique métier
    const user = {
      ...userData,
      role: 'user',
      createdAt: serverTimestamp(),
      verified: false
    };
    
    // Firebase operation
    return await addDoc(collection(db, 'users'), user);
  },
  
  async verifyUser(userId) {
    // Logique métier
    await updateDoc(doc(db, 'users', userId), {
      verified: true,
      verifiedAt: serverTimestamp()
    });
    
    // Déclencher autre logique
    await this.sendWelcomeEmail(userId);
  }
};

# Pattern: Custom Hooks (React)
// Réutiliser logique Firebase

// hooks/useAuth.js
export function useAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user);
      setLoading(false);
    });
    
    return () => unsubscribe();
  }, []);
  
  const login = async (email, password) => {
    return await signInWithEmailAndPassword(auth, email, password);
  };
  
  const logout = async () => {
    return await signOut(auth);
  };
  
  return { user, loading, login, logout };
}

// hooks/useFirestore.js
export function useFirestore(collectionName, queryConstraints = []) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    const q = query(collection(db, collectionName), ...queryConstraints);
    
    const unsubscribe = onSnapshot(
      q,
      (snapshot) => {
        const items = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data()
        }));
        setData(items);
        setLoading(false);
      },
      (error) => {
        setError(error);
        setLoading(false);
      }
    );
    
    return () => unsubscribe();
  }, [collectionName, JSON.stringify(queryConstraints)]);
  
  return { data, loading, error };
}

// Utilisation
function PostsList() {
  const { data: posts, loading } = useFirestore('posts', [
    orderBy('createdAt', 'desc'),
    limit(10)
  ]);
  
  if (loading) return <div>Chargement...</div>;
  
  return (
    <div>
      {posts.map(post => (
        <Post key={post.id} post={post} />
      ))}
    </div>
  );
}

# Pattern: Context Provider
// État global avec Firebase

// contexts/AuthContext.js
const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, async (user) => {
      if (user) {
        // Charger données supplémentaires
        const userDoc = await getDoc(doc(db, 'users', user.uid));
        setUser({ ...user, ...userDoc.data() });
      } else {
        setUser(null);
      }
      setLoading(false);
    });
    
    return () => unsubscribe();
  }, []);
  
  const value = {
    user,
    loading,
    login: async (email, password) => {
      return await signInWithEmailAndPassword(auth, email, password);
    },
    logout: async () => {
      return await signOut(auth);
    }
  };
  
  return (
    <AuthContext.Provider value={value}>
      {!loading && children}
    </AuthContext.Provider>
  );
}

export function useAuth() {
  return useContext(AuthContext);
}

# Pattern: Protected Routes
// Protéger routes avec auth

function ProtectedRoute({ children }) {
  const { user, loading } = useAuth();
  
  if (loading) {
    return <div>Chargement...</div>;
  }
  
  if (!user) {
    return <Navigate to="/login" />;
  }
  
  return children;
}

// Utilisation
<Routes>
  <Route path="/login" element={<Login />} />
  <Route
    path="/dashboard"
    element={
      <ProtectedRoute>
        <Dashboard />
      </ProtectedRoute>
    }
  />
</Routes>

# Pattern: Optimistic Updates
// Mise à jour UI avant confirmation serveur

async function toggleLike(postId, currentlyLiked) {
  // 1. Mettre à jour UI immédiatement
  setLiked(!currentlyLiked);
  setLikeCount(prev => currentlyLiked ? prev - 1 : prev + 1);
  
  try {
    // 2. Mettre à jour serveur
    await updateDoc(doc(db, 'posts', postId), {
      likes: currentlyLiked ? arrayRemove(userId) : arrayUnion(userId)
    });
  } catch (error) {
    // 3. Rollback si erreur
    setLiked(currentlyLiked);
    setLikeCount(prev => currentlyLiked ? prev + 1 : prev - 1);
    console.error('Erreur:', error);
  }
}

# Pattern: Infinite Scroll / Pagination

function useInfiniteScroll(collectionName, pageSize = 10) {
  const [items, setItems] = useState([]);
  const [lastDoc, setLastDoc] = useState(null);
  const [hasMore, setHasMore] = useState(true);
  const [loading, setLoading] = useState(false);
  
  const loadMore = async () => {
    if (loading || !hasMore) return;
    
    setLoading(true);
    
    let q = query(
      collection(db, collectionName),
      orderBy('createdAt', 'desc'),
      limit(pageSize)
    );
    
    if (lastDoc) {
      q = query(q, startAfter(lastDoc));
    }
    
    const snapshot = await getDocs(q);
    
    if (snapshot.empty) {
      setHasMore(false);
    } else {
      const newItems = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      }));
      
      setItems(prev => [...prev, ...newItems]);
      setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
      setHasMore(snapshot.docs.length === pageSize);
    }
    
    setLoading(false);
  };
  
  return { items, loadMore, hasMore, loading };
}


[OK] SÉCURITÉ AVANCÉE


# === APP CHECK ===

# Protection contre bots et abus
# Console Firebase > App Check

// Web (reCAPTCHA)
import { initializeAppCheck, ReCaptchaV3Provider } from 'firebase/app-check';

const appCheck = initializeAppCheck(app, {
  provider: new ReCaptchaV3Provider('RECAPTCHA_SITE_KEY'),
  isTokenAutoRefreshEnabled: true
});

// Règles avec App Check
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.app != null;  // App Check requis
    }
  }
}

# === SÉCURITÉ FUNCTIONS ===

// Vérifier authentification
exports.secureFunction = onRequest(async (request, response) => {
  const token = request.headers.authorization?.split('Bearer ')[1];
  
  if (!token) {
    response.status(401).json({ error: 'Non authentifié' });
    return;
  }
  
  try {
    const decodedToken = await admin.auth().verifyIdToken(token);
    const userId = decodedToken.uid;
    
    // Vérifier rôle
    if (!decodedToken.admin) {
      response.status(403).json({ error: 'Permission refusée' });
      return;
    }
    
    // Logique fonction
    response.json({ success: true });
  } catch (error) {
    response.status(401).json({ error: 'Token invalide' });
  }
});

// Rate limiting
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100  // 100 requêtes max
});

exports.api = onRequest((request, response) => {
  limiter(request, response, () => {
    // Logique API
  });
});

# === VALIDATION DONNÉES ===

// Schema validation avec Joi
const Joi = require('joi');

const userSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(150),
  bio: Joi.string().max(500)
});

exports.createUser = onRequest(async (request, response) => {
  const { error, value } = userSchema.validate(request.body);
  
  if (error) {
    response.status(400).json({
      error: 'Données invalides',
      details: error.details
    });
    return;
  }
  
  // Créer utilisateur avec données validées
  await admin.firestore().collection('users').add(value);
  response.json({ success: true });
});

# === SANITIZATION ===

// Nettoyer entrées utilisateur
const DOMPurify = require('isomorphic-dompurify');

function sanitizeInput(input) {
  return DOMPurify.sanitize(input, {
    ALLOWED_TAGS: ['b', 'i', 'em', 'strong'],
    ALLOWED_ATTR: []
  });
}

// Utilisation
const cleanContent = sanitizeInput(userInput);

# === SECRETS MANAGEMENT ===

# Firebase Functions v2
firebase functions:secrets:set API_KEY

// Utilisation dans function
const { defineSecret } = require('firebase-functions/params');
const apiKey = defineSecret('API_KEY');

exports.callExternalAPI = onRequest(
  { secrets: [apiKey] },
  async (request, response) => {
    const key = apiKey.value();
    // Utiliser clé
  }
);


[OK] TESTING


# === UNIT TESTS (FUNCTIONS) ===

// functions/test/index.test.js
const admin = require('firebase-admin');
const test = require('firebase-functions-test')();

describe('Cloud Functions', () => {
  let adminInitStub;
  
  before(() => {
    adminInitStub = sinon.stub(admin, 'initializeApp');
  });
  
  after(() => {
    adminInitStub.restore();
    test.cleanup();
  });
  
  it('should create user document on auth user created', async () => {
    const wrapped = test.wrap(myFunctions.onNewUser);
    
    const user = {
      uid: 'test123',
      email: 'test@example.com'
    };
    
    await wrapped(user);
    
    // Assertions
    const doc = await admin.firestore()
      .collection('users')
      .doc('test123')
      .get();
    
    assert.equal(doc.data().email, 'test@example.com');
  });
});

# === INTEGRATION TESTS (EMULATORS) ===

// test/integration.test.js
const { initializeApp } = require('firebase/app');
const { 
  getAuth, 
  connectAuthEmulator,
  signInWithEmailAndPassword 
} = require('firebase/auth');
const {
  getFirestore,
  connectFirestoreEmulator
} = require('firebase/firestore');

describe('Integration Tests', () => {
  let app, auth, db;
  
  before(() => {
    app = initializeApp({ projectId: 'test-project' });
    auth = getAuth(app);
    db = getFirestore(app);
    
    connectAuthEmulator(auth, 'http://localhost:9099');
    connectFirestoreEmulator(db, 'localhost', 8080);
  });
  
  it('should create user and save to Firestore', async () => {
    const user = await signInWithEmailAndPassword(
      auth,
      'test@example.com',
      'password123'
    );
    
    const userDoc = await getDoc(doc(db, 'users', user.user.uid));
    expect(userDoc.exists()).toBe(true);
  });
});

# Lancer tests avec émulateurs
firebase emulators:exec --only firestore,auth "npm test"

# === SECURITY RULES TESTS ===

// firestore.test.js
const firebase = require('@firebase/rules-unit-testing');
const fs = require('fs');

const PROJECT_ID = 'test-project';

describe('Firestore Security Rules', () => {
  let testEnv;
  
  beforeAll(async () => {
    testEnv = await firebase.initializeTestEnvironment({
      projectId: PROJECT_ID,
      firestore: {
        rules: fs.readFileSync('firestore.rules', 'utf8')
      }
    });
  });
  
  afterAll(async () => {
    await testEnv.cleanup();
  });
  
  it('should allow user to read own document', async () => {
    const alice = testEnv.authenticatedContext('alice');
    const docRef = alice.firestore().doc('users/alice');
    
    await firebase.assertSucceeds(docRef.get());
  });
  
  it('should deny user reading other user document', async () => {
    const alice = testEnv.authenticatedContext('alice');
    const docRef = alice.firestore().doc('users/bob');
    
    await firebase.assertFails(docRef.get());
  });
});

# Lancer tests
npm test


[OK] PERFORMANCES & OPTIMISATION


# === CACHE PERSISTANT ===

// Activer cache offline Firestore
import { enableIndexedDbPersistence } from 'firebase/firestore';

try {
  await enableIndexedDbPersistence(db);
  console.log('Cache persistant activé');
} catch (error) {
  if (error.code === 'failed-precondition') {
    console.log('Plusieurs onglets ouverts');
  } else if (error.code === 'unimplemented') {
    console.log('Navigateur non supporté');
  }
}

// Multi-tab
enableMultiTabIndexedDbPersistence(db);

# === BUNDLE QUERIES ===

// Charger données initiales avec bundle
import { loadBundle, namedQuery } from 'firebase/firestore';

async function loadInitialData() {
  // Télécharger bundle
  const response = await fetch('/bundles/initial-data.bundle');
  const bundle = await response.text();
  
  // Charger dans Firestore
  await loadBundle(db, bundle);
  
  // Utiliser query nommée du bundle
  const query = await namedQuery(db, 'latest-posts');
  const snapshot = await getDocs(query);
  
  return snapshot.docs.map(doc => doc.data());
}

# Créer bundle (Admin SDK)
const bundle = admin.firestore().bundle('latest-posts-bundle');

const query = admin.firestore()
  .collection('posts')
  .orderBy('createdAt', 'desc')
  .limit(10);

const snapshot = await query.get();

bundle.add('latest-posts', snapshot);
const bundleBuffer = bundle.build();

// Servir bundle via Hosting ou Storage

# === LAZY LOADING ===

// Charger composants à la demande
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Profile = lazy(() => import('./Profile'));

function App() {
  return (
    <Suspense fallback={<div>Chargement...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/profile" element={<Profile />} />
      </Routes>
    </Suspense>
  );
}

# === COMPRESSION IMAGES ===

// Avant upload
async function compressAndUpload(file) {
  const options = {
    maxSizeMB: 1,
    maxWidthOrHeight: 1920,
    useWebWorker: true
  };
  
  const compressedFile = await imageCompression(file, options);
  
  return await uploadFile(compressedFile);
}

# === DEBOUNCE SEARCHES ===

// Éviter trop de requêtes
import { debounce } from 'lodash';

const debouncedSearch = debounce(async (searchTerm) => {
  const q = query(
    collection(db, 'products'),
    where('name', '>=', searchTerm),
    where('name', '<=', searchTerm + '\uf8ff'),
    limit(10)
  );
  
  const snapshot = await getDocs(q);
  setResults(snapshot.docs.map(doc => doc.data()));
}, 300);

# === PRELOAD CRITIQUES ===

// Preload données importantes
useEffect(() => {
  // Charger données user
  if (user) {
    getDoc(doc(db, 'users', user.uid));
    
    // Preload autres données
    getDocs(query(collection(db, 'notifications'), limit(5)));
  }
}, [user]);


[OK] COMMANDES FIREBASE CLI AVANCÉES


# === ALIAS & TARGETS ===

# Définir targets
firebase target:apply hosting production myapp-prod
firebase target:apply hosting staging myapp-staging

# firebase.json
{
  "hosting": [{
    "target": "production",
    "public": "dist",
    "rewrites": [...]
  }, {
    "target": "staging",
    "public": "dist",
    "rewrites": [...]
  }]
}

# Déployer target spécifique
firebase deploy --only hosting:production
firebase deploy --only hosting:staging

# === EXPORTS/IMPORTS ===

# Export émulateurs avec données
firebase emulators:start --export-on-exit=./emulator-data

# Import au démarrage
firebase emulators:start --import=./emulator-data

# === DEBUG ===

# Mode verbose
firebase deploy --debug
firebase functions:log --debug

# Test règles localement
firebase emulators:start --only firestore
# Puis tester via UI: localhost:4000

# === SCRIPTS PERSONNALISÉS ===

# package.json
{
  "scripts": {
    "dev": "firebase emulators:start --import=./data --export-on-exit",
    "deploy:staging": "firebase use staging && firebase deploy",
    "deploy:prod": "firebase use production && firebase deploy",
    "backup": "firebase firestore:export gs://backups/$(date +%Y%m%d)"
  }
}


[OK] FIN DU GUIDE


Ce guide couvre l'essentiel de Firebase pour débuter et aller plus loin.

Points clés à retenir:
[OK] Toujours tester avec émulateurs avant déploiement production
[OK] Sécuriser avec règles strictes Firestore et Storage
[OK] Optimiser performances (cache, pagination, indexes)
[OK] Monitorer coûts et usage régulièrement
[OK] Backup données critiques automatiquement
[OK] Utiliser environnements séparés (dev/staging/prod)

Ressources complémentaires:
- Documentation: https://firebase.google.com/docs
- Console: https://console.firebase.google.com
- Communauté: https://stackoverflow.com/questions/tagged/firebase
- Status: https://status.firebase.google.com

Bon développement avec Firebase! [HOT]