Use Cases (Actualizado: 1/6/2026)

Desarrollo Firebase con Claude Code: guía práctica de implementación

Desarrolla Firebase con Claude Code: Auth, reglas Firestore, Functions, Hosting, Emulator, entornos, costes y seguridad con código real.

Desarrollo Firebase con Claude Code: guía práctica de implementación

Qué decidir antes de pedirle a Claude Code que implemente Firebase

Firebase es una plataforma BaaS muy útil cuando un equipo pequeño necesita autenticación, base de datos, funciones de backend y hosting sin mantener un servidor propio. En la práctica, la pila suele combinar Firebase Authentication, Cloud Firestore, Cloud Functions for Firebase, Firebase Hosting y Local Emulator Suite.

El problema es que una app Firebase puede parecer terminada y seguir siendo insegura. Unas Firestore Security Rules demasiado abiertas pueden exponer documentos de otros usuarios. El Admin SDK usado dentro de Cloud Functions no pasa por las Security Rules, así que las validaciones del servidor se revisan por separado. Y si se mezclan proyectos de desarrollo y producción, una prueba local puede escribir datos reales y generar coste real.

Con Claude Code conviene pedir una funcionalidad vertical, no solo una pantalla. Por ejemplo, “crear tickets de soporte” debe incluir UI, sesión de usuario, escritura en Firestore, reglas, pruebas con Emulator Suite, una Cloud Function para operaciones privilegiadas y configuración de Hosting.

Para bases relacionadas, revisa la guía de autenticación con Claude Code, la guía de CI/CD y la integración con Supabase si quieres comparar opciones de backend.

flowchart LR
  A["User"] --> B["Firebase Authentication"]
  B --> C["React or Astro UI"]
  C --> D["Cloud Firestore"]
  D --> E["Security Rules"]
  D --> F["Cloud Functions v2"]
  C --> G["Firebase Hosting"]
  H["Emulator Suite"] --> B
  H --> D
  H --> F

Usaremos Vite + React + TypeScript para que el código sea fácil de copiar y verificar. En Next.js o Astro, la idea es la misma, pero cambian los nombres de variables de entorno y el enrutamiento.

Arquitectura mínima y documentación oficial

La documentación oficial debe ser la fuente principal. En Firebase, una entrada antigua de blog puede seguir compilando, pero no cubrir bien reglas, emuladores o despliegue.

ÁreaEnlace oficialBuen alcance para Claude Code
AuthenticationFirebase AuthenticationUI de login, perfil de usuario, estado de sesión
FirestoreCloud FirestoreDiseño de colecciones, consultas, índices
Security RulesFirestore Security RulesReglas, pruebas negativas, validación de propietario
Cloud FunctionsCloud Functions for FirebaseValidación servidor, notificaciones, agregaciones
HostingFirebase HostingSPA, caché, canales de vista previa
EmulatorLocal Emulator SuiteValidación local, cobertura de reglas, CI
PreciosFirebase pricingLecturas, escrituras, invocaciones y logs
Claude CodeClaude Code docsTamaño de tareas, revisión y pruebas

Una estructura razonable para que Claude Code la lea primero es esta:

.
├─ firebase.json
├─ firestore.rules
├─ firestore.indexes.json
├─ .firebaserc
├─ functions/
│  ├─ package.json
│  └─ src/index.ts
└─ src/
   ├─ lib/firebase.ts
   ├─ lib/tickets.ts
   └─ lib/useAuth.tsx

Separación de entornos desde el inicio

Muchos incidentes de Firebase no vienen de un bug sofisticado, sino de usar el proyecto equivocado. Define dev, staging y producción antes de escribir lógica de negocio.

{
  "projects": {
    "dev": "claudecodelab-firebase-dev",
    "stg": "claudecodelab-firebase-stg",
    "prod": "claudecodelab-firebase-prod"
  }
}

firebase.json debe versionar reglas, índices, Functions, Hosting y emuladores.

{
  "firestore": {
    "rules": "firestore.rules",
    "indexes": "firestore.indexes.json"
  },
  "functions": [
    {
      "source": "functions",
      "codebase": "default",
      "runtime": "nodejs20"
    }
  ],
  "hosting": {
    "public": "dist",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "headers": [
      {
        "source": "/assets/**",
        "headers": [
          {
            "key": "Cache-Control",
            "value": "public, max-age=31536000, immutable"
          }
        ]
      }
    ],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  },
  "emulators": {
    "auth": {
      "port": 9099
    },
    "functions": {
      "port": 5001
    },
    "firestore": {
      "port": 8080
    },
    "hosting": {
      "port": 5000
    },
    "ui": {
      "enabled": true,
      "port": 4000
    },
    "singleProjectMode": true
  }
}

En Vite, .env.local contiene configuración pública del SDK web. La API key web de Firebase no es una clave de servidor, pero un service account JSON sí es secreto y no debe ir al frontend.

VITE_FIREBASE_API_KEY=replace-me
VITE_FIREBASE_AUTH_DOMAIN=claudecodelab-firebase-dev.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=claudecodelab-firebase-dev
VITE_FIREBASE_STORAGE_BUCKET=claudecodelab-firebase-dev.appspot.com
VITE_FIREBASE_APP_ID=replace-me
VITE_USE_FIREBASE_EMULATORS=true

Authentication e inicialización del cliente

Este archivo inicializa App, Auth, Firestore y Functions, y conecta emuladores en desarrollo.

// src/lib/firebase.ts
import { initializeApp, getApp, getApps } from "firebase/app";
import {
  connectAuthEmulator,
  getAuth,
  GoogleAuthProvider,
} from "firebase/auth";
import { connectFirestoreEmulator, getFirestore } from "firebase/firestore";
import { connectFunctionsEmulator, getFunctions } from "firebase/functions";

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

const app = getApps().length > 0 ? getApp() : initializeApp(firebaseConfig);

export const auth = getAuth(app);
export const db = getFirestore(app);
export const functions = getFunctions(app, "asia-northeast1");
export const googleProvider = new GoogleAuthProvider();

const shouldUseEmulators =
  import.meta.env.DEV && import.meta.env.VITE_USE_FIREBASE_EMULATORS === "true";

const globalState = globalThis as typeof globalThis & {
  __firebaseEmulatorsConnected?: boolean;
};

if (shouldUseEmulators && !globalState.__firebaseEmulatorsConnected) {
  connectAuthEmulator(auth, "http://127.0.0.1:9099", {
    disableWarnings: true,
  });
  connectFirestoreEmulator(db, "127.0.0.1", 8080);
  connectFunctionsEmulator(functions, "127.0.0.1", 5001);
  globalState.__firebaseEmulatorsConnected = true;
}

El estado de sesión queda mejor en un Hook. Al iniciar sesión por primera vez, creamos o actualizamos users/{uid} con merge.

// src/lib/useAuth.tsx
import { useEffect, useState } from "react";
import {
  onAuthStateChanged,
  signInWithPopup,
  signOut,
  type User,
} from "firebase/auth";
import { doc, serverTimestamp, setDoc } from "firebase/firestore";
import { auth, db, googleProvider } from "./firebase";

type AuthState = {
  user: User | null;
  loading: boolean;
  signInWithGoogle: () => Promise<void>;
  logout: () => Promise<void>;
};

export function useAuth(): AuthState {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    return onAuthStateChanged(auth, (currentUser) => {
      setUser(currentUser);
      setLoading(false);
    });
  }, []);

  async function signInWithGoogle() {
    const result = await signInWithPopup(auth, googleProvider);
    await setDoc(
      doc(db, "users", result.user.uid),
      {
        uid: result.user.uid,
        email: result.user.email,
        displayName: result.user.displayName,
        photoURL: result.user.photoURL,
        updatedAt: serverTimestamp(),
      },
      { merge: true },
    );
  }

  return {
    user,
    loading,
    signInWithGoogle,
    logout: () => signOut(auth),
  };
}

Una petición útil para Claude Code sería:

Implementa Google sign-in con Firebase Auth.
- Stack: Vite + React + TypeScript
- Reutiliza src/lib/firebase.ts
- Crea users/{uid} con merge en el primer login
- Devuelve función logout
- No crees ni muestres credenciales de service account
- Explica errores de tipos, impacto en reglas Firestore y comprobaciones manuales

Modelo Firestore y CRUD

El ejemplo es un sistema de tickets de soporte. Cubre tres usos reales: portal de miembros donde cada usuario ve sus tickets, flujo de soporte donde una función cierra tickets y panel interno con notificaciones al crear tickets.

// src/lib/tickets.ts
import {
  addDoc,
  collection,
  getDocs,
  limit,
  orderBy,
  query,
  serverTimestamp,
  where,
  type Timestamp,
} from "firebase/firestore";
import { db } from "./firebase";

export type TicketStatus = "open" | "closed";

export type Ticket = {
  id: string;
  userId: string;
  title: string;
  body: string;
  status: TicketStatus;
  createdAt: Timestamp;
  updatedAt: Timestamp;
};

type CreateTicketInput = {
  userId: string;
  title: string;
  body: string;
};

export async function createTicket(input: CreateTicketInput): Promise<string> {
  const title = input.title.trim();
  const body = input.body.trim();

  if (title.length === 0 || title.length > 120) {
    throw new Error("Title must be between 1 and 120 characters.");
  }

  if (body.length === 0 || body.length > 4000) {
    throw new Error("Body must be between 1 and 4000 characters.");
  }

  const docRef = await addDoc(collection(db, "tickets"), {
    userId: input.userId,
    title,
    body,
    status: "open",
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp(),
  });

  return docRef.id;
}

export async function listMyTickets(userId: string): Promise<Ticket[]> {
  const ticketsQuery = query(
    collection(db, "tickets"),
    where("userId", "==", userId),
    orderBy("createdAt", "desc"),
    limit(20),
  );

  const snapshot = await getDocs(ticketsQuery);

  return snapshot.docs.map((ticketDoc) => ({
    id: ticketDoc.id,
    ...(ticketDoc.data() as Omit<Ticket, "id">),
  }));
}

La combinación where y orderBy puede requerir un índice compuesto. Guárdalo en el repositorio.

{
  "indexes": [
    {
      "collectionGroup": "tickets",
      "queryScope": "COLLECTION",
      "fields": [
        {
          "fieldPath": "userId",
          "order": "ASCENDING"
        },
        {
          "fieldPath": "createdAt",
          "order": "DESCENDING"
        }
      ]
    }
  ],
  "fieldOverrides": []
}

Firestore Security Rules no son filtros

Las reglas no filtran una consulta amplia para dejar solo documentos seguros. La consulta debe estar diseñada para que todos sus resultados posibles estén permitidos.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function signedIn() {
      return request.auth != null;
    }

    function isOwner(userId) {
      return signedIn() && request.auth.uid == userId;
    }

    function ticketFieldsAreValid() {
      return request.resource.data.keys().hasOnly([
        "userId",
        "title",
        "body",
        "status",
        "createdAt",
        "updatedAt"
      ])
      && request.resource.data.title is string
      && request.resource.data.title.size() > 0
      && request.resource.data.title.size() <= 120
      && request.resource.data.body is string
      && request.resource.data.body.size() > 0
      && request.resource.data.body.size() <= 4000;
    }

    match /users/{userId} {
      allow create, read, update: if isOwner(userId);
      allow delete: if false;
    }

    match /tickets/{ticketId} {
      allow create: if signedIn()
        && request.resource.data.userId == request.auth.uid
        && request.resource.data.status == "open"
        && ticketFieldsAreValid();

      allow read: if signedIn()
        && resource.data.userId == request.auth.uid;

      allow update: if signedIn()
        && resource.data.userId == request.auth.uid
        && request.resource.data.userId == resource.data.userId
        && request.resource.data.status == resource.data.status
        && request.resource.data.diff(resource.data).affectedKeys()
          .hasOnly(["title", "body", "updatedAt"])
        && ticketFieldsAreValid();

      allow delete: if false;
    }

    match /adminStats/{docId} {
      allow read, write: if false;
    }
  }
}

Revisa la comparación entre request.auth.uid y el propietario, la lista cerrada de campos, la prohibición de borrar y que los datos administrativos no estén abiertos al cliente.

Pruebas con Emulator Suite

npm install -D vitest @firebase/rules-unit-testing firebase
firebase setup:emulators:firestore
// tests/firestore.rules.test.ts
import { readFileSync } from "node:fs";
import {
  assertFails,
  assertSucceeds,
  initializeTestEnvironment,
  type RulesTestEnvironment,
} from "@firebase/rules-unit-testing";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { doc, getDoc, setDoc, updateDoc } from "firebase/firestore";

let testEnv: RulesTestEnvironment;

beforeAll(async () => {
  testEnv = await initializeTestEnvironment({
    projectId: "claudecodelab-firestore-rules",
    firestore: {
      rules: readFileSync("firestore.rules", "utf8"),
    },
  });
});

beforeEach(async () => {
  await testEnv.clearFirestore();
});

afterAll(async () => {
  await testEnv.cleanup();
});

describe("tickets security rules", () => {
  it("allows the owner to create and read a ticket", async () => {
    const aliceDb = testEnv.authenticatedContext("alice").firestore();
    const ticketRef = doc(aliceDb, "tickets/ticket-1");

    await assertSucceeds(
      setDoc(ticketRef, {
        userId: "alice",
        title: "Please resend my invoice",
        body: "I cannot find the April invoice.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      }),
    );

    await assertSucceeds(getDoc(ticketRef));
  });

  it("blocks another user from reading the ticket", async () => {
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await setDoc(doc(context.firestore(), "tickets/ticket-2"), {
        userId: "alice",
        title: "Plan question",
        body: "I want to confirm my current plan.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      });
    });

    const bobDb = testEnv.authenticatedContext("bob").firestore();
    await assertFails(getDoc(doc(bobDb, "tickets/ticket-2")));
  });

  it("blocks status changes from the web client", async () => {
    await testEnv.withSecurityRulesDisabled(async (context) => {
      await setDoc(doc(context.firestore(), "tickets/ticket-3"), {
        userId: "alice",
        title: "Cannot sign in",
        body: "Google sign-in returns an error.",
        status: "open",
        createdAt: new Date(),
        updatedAt: new Date(),
      });
    });

    const aliceDb = testEnv.authenticatedContext("alice").firestore();
    await assertFails(
      updateDoc(doc(aliceDb, "tickets/ticket-3"), {
        status: "closed",
        updatedAt: new Date(),
      }),
    );
  });

  it("keeps the test runner alive", () => {
    expect(testEnv).toBeDefined();
  });
});
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "firebase:use:dev": "firebase use dev",
    "firebase:emulators": "firebase emulators:start --only auth,firestore,functions,hosting",
    "test:rules": "firebase emulators:exec --only firestore \"vitest run tests/firestore.rules.test.ts\"",
    "deploy:stg": "firebase use stg && npm run build && firebase deploy --only hosting,firestore:rules,firestore:indexes,functions",
    "deploy:prod": "firebase use prod && npm run build && firebase deploy --only hosting,firestore:rules,firestore:indexes,functions"
  }
}

Cloud Functions para operaciones privilegiadas

Usa Functions para cambios de estado, claves externas, notificaciones y agregaciones que no deben depender del navegador.

// functions/src/index.ts
import { initializeApp } from "firebase-admin/app";
import { FieldValue, getFirestore } from "firebase-admin/firestore";
import { onDocumentCreated } from "firebase-functions/v2/firestore";
import { HttpsError, onCall } from "firebase-functions/v2/https";

initializeApp();

const db = getFirestore();

export const closeTicket = onCall(
  {
    region: "asia-northeast1",
  },
  async (request) => {
    if (!request.auth) {
      throw new HttpsError("unauthenticated", "Sign-in is required.");
    }

    const ticketId = request.data?.ticketId;
    if (typeof ticketId !== "string" || ticketId.length > 100) {
      throw new HttpsError("invalid-argument", "ticketId is invalid.");
    }

    const ticketRef = db.doc(`tickets/${ticketId}`);
    const ticketSnap = await ticketRef.get();

    if (!ticketSnap.exists) {
      throw new HttpsError("not-found", "Ticket was not found.");
    }

    const ticket = ticketSnap.data();
    if (ticket?.userId !== request.auth.uid) {
      throw new HttpsError("permission-denied", "You cannot close this ticket.");
    }

    await ticketRef.update({
      status: "closed",
      closedAt: FieldValue.serverTimestamp(),
      updatedAt: FieldValue.serverTimestamp(),
    });

    return { ok: true };
  },
);

export const notifyTicketCreated = onDocumentCreated(
  {
    document: "tickets/{ticketId}",
    region: "asia-northeast1",
  },
  async (event) => {
    const ticket = event.data?.data();
    if (!ticket) return;

    await db.collection("adminNotifications").add({
      type: "ticket_created",
      ticketId: event.params.ticketId,
      title: ticket.title,
      userId: ticket.userId,
      createdAt: FieldValue.serverTimestamp(),
      read: false,
    });
  },
);

El punto crítico es que el Admin SDK omite Security Rules. Por eso cada Callable Function debe comprobar request.auth, validar entradas y confirmar el propietario del documento.

Hosting, costes y seguridad

firebase login
firebase use dev
npm run build
firebase emulators:start --only auth,firestore,functions,hosting
firebase hosting:channel:deploy preview-firebase-ticket
firebase use stg
firebase deploy --only hosting,firestore:rules,firestore:indexes,functions

Fallos típicos: permitir lectura y escritura a cualquier usuario autenticado, leer toda la colección y filtrar en React, omitir validación de propietario en Functions, probar con un emulador que no carga las mismas reglas, o mezclar .env.local, firebase use y la consola de Firebase.

Para costes, revisa la página oficial de Firebase pricing. Mira lecturas de Firestore, limit, listeners en tiempo real, región y timeout de Functions, volumen de logs y caché de Hosting. En seguridad, no pegues service account JSON, tokens de CI ni permisos Owner de producción en el prompt de Claude Code.

Plantilla de prompt para Claude Code

Implementa la función de tickets de soporte con Firebase.

Alcance:
- Vite + React + TypeScript
- Firebase Auth, Firestore, Cloud Functions v2, Hosting
- Edita solo src/lib/firebase.ts, src/lib/useAuth.tsx, src/lib/tickets.ts, firestore.rules, functions/src/index.ts, firebase.json

Requisitos:
- Usuarios con Google login pueden crear tickets
- Cada usuario solo puede leer sus propios tickets
- El cliente web no puede cambiar status
- Una Callable Function cierra tickets tras validar propietario
- Agrega pruebas de Emulator Suite para accesos permitidos y denegados
- Mantén separados dev, stg y prod

Prohibido:
- Crear, imprimir o guardar service account JSON
- Ejecutar deploy de producción
- Usar allow read, write: if true

Reporte:
- Archivos cambiados
- Pruebas ejecutadas
- Límite de permisos en las reglas
- Comprobaciones manuales pendientes

Conclusión

Claude Code funciona bien con Firebase porque reglas, funciones, índices, Hosting y SDK cliente generan diferencias revisables. Lo que no conviene delegar sin control es la decisión de permisos, el deploy de producción, la propiedad de datos y el presupuesto.

En un proyecto real, crea primero una funcionalidad pequeña en Emulator Suite, añade pruebas negativas de Security Rules, revisa la autorización en Cloud Functions y publica por un canal de preview antes de staging y producción. ClaudeCodeLab puede apoyar implementación Firebase, revisión de Security Rules, formación en Emulator Suite y flujos de equipo con Claude Code.

Al probar este artículo, confirma que usas el proyecto Firebase de desarrollo, que firebase use coincide con .env.local, que las pruebas de reglas pasan tanto en casos permitidos como rechazados, que Cloud Functions valida el propietario internamente y que Claude Code no tiene permiso para ejecutar despliegues de producción automáticamente.

#Claude Code #Firebase #Firestore #Cloud Functions #BaaS
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.