npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

zod-firebase

v2.1.2

Published

zod firebase schema

Readme

zod-firebase

npm version License: MIT CI codecov

Type-safe Firestore collections and documents using Zod schemas for the Firebase Web SDK.

Installation

Peer dependencies: firebase and zod.

npm install zod-firebase zod firebase

Usage

Basic Setup

First, define your document schemas using Zod:

import { z } from 'zod'
import { collectionsBuilder } from 'zod-firebase'

// Define your document schemas
const UserSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().optional(),
  tags: z.array(z.string()).optional().default([]),
})

const PostSchema = z.object({
  title: z.string(),
  content: z.string(),
  authorId: z.string(),
  publishedAt: z.date(),
  likes: z.number().default(0),
})

// Define your collection schema
const schema = {
  users: {
    zod: UserSchema,
  },
  posts: {
    zod: PostSchema,
  },
} as const

// Build type-safe collections
const collections = collectionsBuilder(schema)

CRUD Operations

// Create a new user
const userRef = await collections.users.add({
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
})

// Get a user by ID
const user = await collections.users.findByIdOrThrow(userRef.id)
console.log(user._id, user.name, user.email) // Fully typed!

// Update a user
await collections.users.update(userRef.id, {
  age: 31,
})

// Query users
const adults = await collections.users.findMany({
  name: 'adults',
  where: [['age', '>=', 18]],
})

// Delete a user
await collections.users.delete(userRef.id)

Fallback reads

Return a document if it exists or a validated fallback when it does not.

// Multi-document collection: findByIdWithFallback(id, fallback)
const post = await collections.posts.findByIdWithFallback('post123', {
  title: 'Untitled',
  content: '',
  authorId: 'anonymous',
  publishedAt: new Date(),
  likes: 0,
})
// If the document exists, you get its data; otherwise you get:
// { _id: 'post123', title: 'Untitled', content: '', ... }

// Single-document collection: findWithFallback(fallback)
const userId = 'user123'
const profile = await collections.users(userId).profile.findWithFallback({
  bio: 'This user has not set up a bio yet',
  avatar: undefined,
})
// If the document does not exist, you get { _id: 'profile', ...fallback }

When your schema validates document IDs (includeDocumentIdForZod), the fallback is validated with the injected _id:

const UserWithIdSchema = z.discriminatedUnion('_id', [
  z.object({
    _id: z.literal('admin'),
    name: z.string(),
    role: z.literal('administrator'),
  }),
  z.object({
    _id: z.string(),
    name: z.string(),
    role: z.literal('user'),
  }),
])

const schema = {
  users: {
    zod: UserWithIdSchema,
    includeDocumentIdForZod: true,
  },
} as const

const collections = collectionsBuilder(schema)

// Fallback excludes _id; it will be injected and validated by Zod
const admin = await collections.users.findByIdWithFallback('admin', {
  name: 'System',
  role: 'administrator',
})

Sub-Collections

You can define nested sub-collections with full type safety:

const schema = {
  users: {
    zod: UserSchema,
    posts: {
      zod: PostSchema,
      // Sub-sub-collections
      comments: {
        zod: z.object({
          text: z.string(),
          authorId: z.string(),
          createdAt: z.date(),
        }),
      },
    },
    // Single document sub-collection
    profile: {
      zod: z.object({
        bio: z.string(),
        avatar: z.string().optional(),
      }),
      singleDocumentKey: 'profile', // Fixed document ID
    },
  },
} as const

const collections = collectionsBuilder(schema)

// Working with sub-collections
const userId = 'user123'

// Add a post to user's posts sub-collection
const postRef = await collections.users(userId).posts.add({
  title: 'My First Post',
  content: 'Hello world!',
  authorId: userId,
  publishedAt: new Date(),
})

// Add a comment to the post
await collections.users(userId).posts(postRef.id).comments.add({
  text: 'Great post!',
  authorId: 'commenter123',
  createdAt: new Date(),
})

// Work with single document sub-collection
await collections.users(userId).profile.set({
  bio: 'Software developer',
  avatar: 'https://example.com/avatar.jpg',
})

const profile = await collections.users(userId).profile.findOrThrow()

Collection Group Queries

Query across all sub-collections of the same type:

// Find all posts across all users
const allPosts = await collections.users.posts.group.findMany({
  name: 'posts-since-2024',
  where: [['publishedAt', '>=', new Date('2024-01-01')]],
})

// Aggregate with the Firestore Web SDK (e.g. count)
import { count } from 'firebase/firestore'
const totals = await collections.users.posts.comments.group.aggregateFromServer(
  { name: 'all-comments' },
  { total: count() },
)
console.log(totals.total)

Advanced Options

Custom Error Handling

const collections = collectionsBuilder(schema, {
  zodErrorHandler: (error, snapshot) => {
    console.error(`Validation error for document ${snapshot.id}:`, error)
    return new Error(`Invalid document: ${snapshot.id}`)
  },
})

Data Transformation

Handle Firebase-specific data types like Timestamps:

import { Timestamp } from 'firebase/firestore'

const collections = collectionsBuilder(schema, {
  snapshotDataConverter: (snapshot) => {
    const data = snapshot.data()
    // Convert Firestore Timestamps to JavaScript Dates
    return Object.fromEntries(
      Object.entries(data).map(([key, value]) => [key, value instanceof Timestamp ? value.toDate() : value]),
    )
  },
})

Document ID Validation

Include document IDs in Zod validation:

const UserWithIdSchema = z.discriminatedUnion('_id', [
  z.object({
    _id: z.literal('admin'),
    name: z.string(),
    role: z.literal('administrator'),
  }),
  z.object({
    _id: z.string(),
    name: z.string(),
    role: z.literal('user'),
  }),
])

const schema = {
  users: {
    zod: UserWithIdSchema,
    includeDocumentIdForZod: true,
  },
} as const

Read-Only Documents

Mark collections as read-only to prevent accidental modifications:

const schema = {
  config: {
    zod: ConfigSchema,
    readonlyDocuments: true,
  },
} as const

// This collection will only have read operations available
const collections = collectionsBuilder(schema)

Query Features

Advanced Queries

// Complex queries with multiple conditions
const recentPopularPosts = await collections.posts.findMany({
  name: 'recent-popular',
  where: [
    ['publishedAt', '>=', new Date('2024-01-01')],
    ['likes', '>=', 100],
  ],
  orderBy: [['likes', 'desc']],
  limit: 10,
})

// Prepare once, execute with the Web SDK
import { getDocs } from 'firebase/firestore'
const popularPrepared = collections.posts.prepare({
  name: 'popular',
  where: [['likes', '>=', 100]],
  orderBy: [['likes', 'desc']],
})
const snapshot = await getDocs(popularPrepared)
const results = snapshot.docs.map((d) => d.data())

QuerySpecification

The QuerySpecification accepted by prepare, query, and find* supports the following parameters:

  • name: A label for your query, used in error messages
  • constraints?: Prebuilt Web SDK QueryConstraint[] passed directly to query(...)
  • where?: Array of tuples [field, op, value]
  • orderBy?: Array of tuples [field] or [field, 'asc' | 'desc']
  • limit?: Maximum number of results
  • limitToLast?: Returns the last N results; requires a matching orderBy
  • startAt? | startAfter? | endAt? | endBefore?: Cursor boundaries, each can be a document snapshot or an array of field values
    • Arrays are forwarded as individual arguments (e.g. startAt(...values)). Ensure the order matches your orderBy
    • Docs: Query cursors

Related:

  • Collection group queries: Guide
  • Aggregate queries (e.g., count): Guide

Metadata Access

Access Firestore metadata when needed:

// Get document with metadata
const userWithMeta = await collections.users.findByIdOrThrow(userId, {
  _metadata: true,
})

console.log(userWithMeta._metadata?.hasPendingWrites)

API Reference

Types

  • DocumentInput<Z>: Input type inferred with z.input<Z> from your Zod schema Z (data you write). See Zod’s input/output docs
  • DocumentOutput<Z, Options>: Output type inferred from your Zod schema Z using z.output<Z>, optionally augmented with metadata depending on Options:
    • _id (string) included by default unless { _id: false }
    • _metadata (Firestore SnapshotMetadata) when { _metadata: true }
    • When { readonly: true }, the data portion is deeply readonly
  • SchemaDocumentInput<TCollectionSchema>: Input type for a collection built from a Zod schema; accepts either the input type or a deeply readonly version of it
  • SchemaDocumentOutput<TCollectionSchema, Options>: Output type for a collection built from a Zod schema, mirroring DocumentOutput behavior and honoring readonlyDocuments in the collection schema

Examples:

import { z } from 'zod'

const User = z.object({
  name: z.string(),
  age: z.number().optional(),
})

type UserInput = DocumentInput<typeof User> // { name: string; age?: number }
type UserOutput = DocumentOutput<typeof User> // { _id: string; name: string; age?: number }
type UserOutputWithMeta = DocumentOutput<typeof User, { _metadata: true }>
// { _id: string; _metadata: SnapshotMetadata; name: string; age?: number }

// With collectionsBuilder
const schema = {
  users: { zod: User },
} as const

type UsersInput = SchemaDocumentInput<typeof schema.users>
// { name: string; age?: number } | ReadonlyDeep<{ name: string; age?: number }>

type UsersOutput = SchemaDocumentOutput<typeof schema.users>
// { _id: string; name: string; age?: number }

// If the collection is marked as readonly:
const readonlySchema = {
  users: { zod: User, readonlyDocuments: true },
} as const

type ReadonlyUsersOutput = SchemaDocumentOutput<typeof readonlySchema.users>
// ReadonlyDeep<{ name: string; age?: number }> & { _id: string }

Collection Methods

  • add(data) - Add a new document
  • set(id, data, options?) - Set document data (create or overwrite)
  • update(id, data, options?) - Update document fields
  • delete(id) - Delete a document
  • findById(id, options?) - Find document by ID (returns undefined if not found)
  • findByIdOrThrow(id, options?) - Find document by ID (throws if not found)
  • prepare(query) - Prepare a typed query for reuse
  • query(query) - Execute a query and return a QuerySnapshot
  • findMany(query) - Query multiple documents and return data[]
  • aggregateFromServer(query, aggregateSpec) - Server-side aggregates (e.g. count)

Sub-Collection Access

  • collection(parentId).subCollection - Access sub-collection
  • collection.subCollection.group - Access collection group

Configuration Options

  • zodErrorHandler - Custom error handling for validation failures
  • snapshotDataConverter - Transform document data before validation
  • includeDocumentIdForZod - Include document ID in Zod validation
  • readonlyDocuments - Mark collection as read-only
  • singleDocumentKey - Create single-document sub-collections

License

MIT

Contributing

See the main repository at valian-ca/zod-firebase-admin for contributing guidelines.