Use Cases

Firestore Schema Design with Claude Code: Start from Queries, Not Collections

A practical Firestore design workflow using Claude Code: query-first schema design, indexes, costs, security rules, and working TypeScript examples.

Firestore design starts with questions, not tables

I am Masa, the operator of claudecode-lab.com.

When I first used Firestore, I designed it like a relational database: users, posts, comments, and so on. That looked clean for the first few screens, then it started hurting. The moment I needed “latest published posts by author”, “drafts ordered by update time”, and “tag pages by language”, the schema no longer matched the product.

Firestore is not a database where you casually fix everything later with joins. The safer workflow is to list the questions your app asks, then design the documents around those queries.

This article shows the workflow I now use with Claude Code: query list first, schema second, index file third, security rules alongside the model. It follows the guidance in the official Firestore best practices, index overview, and pricing docs:


Step 1: Ask Claude Code for a query inventory

Do not begin with collection names. Begin with screens.

claude -p "
Design Firestore for a media CMS.
List the queries before suggesting collections.

Screens:
- Published post list by publishedAt desc
- Author page by authorId and publishedAt desc
- Admin draft list by updatedAt desc
- Tag page by tag slug
- Contact inbox by status and createdAt asc

Return where/orderBy/limit and required composite indexes.
"

The useful output is a table:

ScreenwhereorderByIndex
Post liststatus == "published"publishedAt descstatus, publishedAt desc
Author pageauthorId, statuspublishedAt descauthorId, status, publishedAt desc
Draft adminstatus == "draft"updatedAt descstatus, updatedAt desc
Tag pagetagSlugs array-contains, statuspublishedAt desctagSlugs, status, publishedAt desc

This table keeps the schema grounded in real UI needs.


Step 2: Denormalize small display fields deliberately

For a post list, I usually store the author name and tag names on the post document. It is duplication, but it avoids extra reads per row.

export interface PostDoc {
  id: string;
  slug: string;
  title: string;
  description: string;
  status: "draft" | "published" | "archived";
  lang: "ja" | "en" | "es" | "ko";
  authorId: string;
  authorName: string;
  authorAvatarUrl: string;
  tagSlugs: string[];
  tagNames: string[];
  publishedAt: FirebaseFirestore.Timestamp | null;
  updatedAt: FirebaseFirestore.Timestamp;
  createdAt: FirebaseFirestore.Timestamp;
}

If author metadata changes, update duplicated fields with a Cloud Function. The list page stays a single query.


Step 3: Validate writes with Zod

Firestore accepts flexible documents. That flexibility becomes pain when publishedAt: null and missing publishedAt start mixing.

import { z } from "zod";

export const CreatePostSchema = z.object({
  slug: z.string().min(3).max(120).regex(/^[a-z0-9-]+$/),
  title: z.string().min(1).max(120),
  description: z.string().max(160),
  lang: z.enum(["ja", "en", "es", "ko"]),
  authorId: z.string().min(1),
  authorName: z.string().min(1),
  authorAvatarUrl: z.string().url(),
  tagSlugs: z.array(z.string()).max(8),
  tagNames: z.array(z.string()).max(8),
});
export async function createDraftPost(input: unknown) {
  const parsed = CreatePostSchema.parse(input);
  const ref = db.collection("posts").doc();

  await ref.set({
    id: ref.id,
    ...parsed,
    status: "draft",
    publishedAt: null,
    createdAt: FieldValue.serverTimestamp(),
    updatedAt: FieldValue.serverTimestamp(),
  });

  return ref.id;
}

Ask Claude Code to review both the TypeScript type and the runtime schema. Do not let Firestore become a dumping ground for half-shaped objects.


Step 4: Put indexes in Git

Console-generated index links are convenient, but they hide design decisions. I prefer to commit firestore.indexes.json.

{
  "indexes": [
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "lang", "order": "ASCENDING" },
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "publishedAt", "order": "DESCENDING" }
      ]
    }
  ],
  "fieldOverrides": [
    {
      "collectionGroup": "posts",
      "fieldPath": "body",
      "indexes": []
    }
  ]
}

Long body fields and large arrays should not be indexed unless your query actually needs them.


Step 5: Review rules with attack scenarios

Do not ask Claude Code for “secure rules”. Give it attacks to block.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    function isAdmin() {
      return request.auth != null
        && request.auth.token.role == "admin";
    }

    match /posts/{postId} {
      allow read: if resource.data.status == "published" || isAdmin();
      allow create, update, delete: if isAdmin();
    }

    match /contacts/{contactId} {
      allow create: if true;
      allow read, update, delete: if isAdmin();
    }
  }
}

Then test attacks: anonymous users reading drafts, normal users changing status, and contact form submissions injecting admin fields.


Pitfalls I actually hit

  1. Sequential document IDs can create hotspots. Use automatic document IDs and store slugs separately.
  2. Array fields are convenient, but heavy tag usage may deserve a tagPosts collection.
  3. Pricing follows document reads, index reads, storage, and bandwidth. Think in reads per screen, not just page views.
  4. Security rules and schema are one design problem, not two.

When I applied this workflow to three small Firestore designs, I rewrote more than half of my first draft. The biggest improvement came from asking Claude Code for the query table before asking for code. Firestore rewards that discipline.

#claude-code #gcp #firestore #database #typescript #query-design
Free

Free PDF: Claude Code Cheatsheet in 5 Minutes

Just enter your email and we'll send you the single-page A4 cheatsheet right away.

We handle your data with care and never send spam.

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Masa

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.