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:
| Screen | where | orderBy | Index |
|---|---|---|---|
| Post list | status == "published" | publishedAt desc | status, publishedAt desc |
| Author page | authorId, status | publishedAt desc | authorId, status, publishedAt desc |
| Draft admin | status == "draft" | updatedAt desc | status, updatedAt desc |
| Tag page | tagSlugs array-contains, status | publishedAt desc | tagSlugs, 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
- Sequential document IDs can create hotspots. Use automatic document IDs and store slugs separately.
- Array fields are convenient, but heavy tag usage may deserve a
tagPostscollection. - Pricing follows document reads, index reads, storage, and bandwidth. Think in reads per screen, not just page views.
- 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.
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.
About the Author
Masa
Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.
Related Posts
What Are Codex Automations? How to Let AI Run Content Ops While You Sleep
A practical guide to using Codex Automations for analytics, article planning, publishing, and monetization workflows.
Claude Code × GCP Cloud Functions Complete Guide | Rapid Serverless Function Development
Streamline GCP Cloud Functions with Claude Code. Implement HTTP/Pub/Sub/Firestore triggers, local testing, and deployment automation with real-world code examples from Masa's experience.
Claude Code × GCP Cloud Run Complete Guide | Serverless Container Auto-Deployment
Speed up GCP Cloud Run deployments with Claude Code. Complete guide with real code examples: Dockerfile generation, auto-scaling, CI/CD pipelines, and Secret Manager integration.