zod-firebase-admin
v2.1.1
Published
zod firebase-admin schema
Downloads
2,311
Maintainers
Readme
zod-firebase-admin
Type-safe Firestore collections and documents using Zod schemas for the Firebase Admin SDK.
Installation
Peer dependencies: firebase-admin and zod.
npm install zod-firebase-admin zod firebase-adminNode.js >= 22 is recommended (matches the library engines field). ESM and CJS bundles are provided.
Usage
Basic Setup
First, define your document schemas using Zod:
import { z } from 'zod'
import { collectionsBuilder } from 'zod-firebase-admin'
// 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:
// Count all posts across all users
const postCount = await collections.users.posts.group.count({
name: 'all-posts',
})
// Count all comments across all posts
const commentCount = await collections.users.posts.comments.group.count({
name: 'all-comments',
})Advanced Schema 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-admin/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 constRead-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)Transactions and Batches
Work with Firebase Admin transactions and batch writes:
import { getFirestore } from 'firebase-admin/firestore'
const db = getFirestore()
// Using transactions
await db.runTransaction(async (transaction) => {
const userSnap = await transaction.get(collections.users.read.doc('user123'))
if (!userSnap.exists) return
transaction.update(collections.users.write.doc('user123'), { age: FieldValue.increment(1) })
})
// Using batch writes
const batch = db.batch()
batch.create(collections.posts.write.doc('post123'), {
title: 'Batch Post',
authorId: 'user123',
})
await batch.commit()Firebase Admin SDK Features
Take advantage of Firebase Admin SDK server-side capabilities:
// Precondition checks with last update time
const user = await collections.users.findByIdOrThrow('user123', {
_updateTime: true,
})
await collections.users.update(
'user123',
{
name: 'Updated Name',
},
{
lastUpdateTime: user._updateTime, // Prevents concurrent modifications
},
)
// Server timestamps and field values
import { FieldValue } from 'firebase-admin/firestore'
await collections.users.update('user123', {
lastLoginAt: FieldValue.serverTimestamp(),
loginCount: FieldValue.increment(1),
})Query Features
Advanced Queries
// Complex queries with multiple conditions
const recentPopularPosts = await collections.posts.findMany({
where: [
['publishedAt', '>=', new Date('2024-01-01')],
['likes', '>=', 100],
],
orderBy: [['likes', 'desc']],
limit: 10,
})
// Prepared queries for reuse
const popularPrepared = collections.posts.prepare({
name: 'popular',
where: [['likes', '>=', 100]],
orderBy: [['likes', 'desc']],
})
const snapshot = await popularPrepared.get()
const results = snapshot.docs.map((d) => d.data())QuerySpecification
The QuerySpecification accepted by prepare, query, find*, and count supports:
- name: A label for your query, used in error messages
- where?: Either an array of tuples
[field, op, value]or an AdminFilter- Tuples: Simple queries
- Filters: Admin Filter API
- orderBy?: Array of tuples
[field]or[field, 'asc' | 'desc']- Docs: Order and limit data
- limit?: Maximum number of results
- Docs: Order and limit data
- limitToLast?: Returns the last N results; requires a matching
orderBy- Docs: Order and limit data
- offset?: Skip the first N results
- Docs: Pagination with offset
- 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 yourorderBy - Docs: Query cursors
- Arrays are forwarded as individual arguments (e.g.
Related:
- Collection group queries: Guide
- Aggregations via Admin SDK
count(): Admin Query.count
Metadata Access
Access Firestore metadata when needed:
// Get document with metadata
const userWithMeta = await collections.users.findByIdOrThrow(userId, {
_createTime: true,
_updateTime: true,
})
console.log(userWithMeta._createTime, userWithMeta._updateTime)API Reference
Types
DocumentInput<Z>: Input type inferred withz.input<Z>from your Zod schemaZ(data you write). See Zod’s input/output docsDocumentOutput<Z, Options>: Output type inferred withz.output<Z>from your Zod schemaZ, plus optional metadata based onOptions:_id(string) included by default unless{ _id: false }_createTime/_updateTimeavailable via operation options when reading,_metadatafor web parity is not used here- 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 itSchemaDocumentOutput<TCollectionSchema, Options>: Output type for a collection built from a Zod schema, mirroringDocumentOutputbehavior and honoringreadonlyDocumentsin the collection schema
Examples:
import { z } from 'zod'
const User = z.object({
name: z.string(),
admin: z.boolean().default(false),
})
type UserInput = DocumentInput<typeof User>
type UserOutput = DocumentOutput<typeof User>
type ReadonlyUserOutput = DocumentOutput<typeof User, { readonly: true }>
// With collectionsBuilder
const schema = {
users: { zod: User },
} as const
type UsersInput = SchemaDocumentInput<typeof schema.users>
// { name: string; admin?: boolean } | ReadonlyDeep<{ name: string; admin?: boolean }>
type UsersOutput = SchemaDocumentOutput<typeof schema.users>
// { _id: string; name: string; admin: boolean }
// Include Admin metadata in outputs
type UsersOutputWithTimes = SchemaDocumentOutput<typeof schema.users, { _createTime: true; _updateTime: true }>
// { _id: string; _createTime: Timestamp; _updateTime: Timestamp; name: string; admin: boolean }
// Readonly collection
const readonlySchema = {
users: { zod: User, readonlyDocuments: true },
} as const
type ReadonlyUsersOutput = SchemaDocumentOutput<typeof readonlySchema.users>
// ReadonlyDeep<{ name: string; admin: boolean }> & { _id: string }Collection Methods
add(data)- Add a new document with auto-generated IDcreate(id, data)- Create a document with specific IDset(id, data, options?)- Set document data (overwrites)update(id, data, options?)- Update document fieldsdelete(id, options?)- Delete a documentfindById(id, options?)- Find document by ID (returns undefined if not found)findByIdOrThrow(id, options?)- Find document by ID (throws if not found)findMany(query)- Query multiple documentscount(query)- Count documents matching queryprepare(query)- Prepare a query for reuse
Sub-Collection Access
collection(parentId).subCollection- Access sub-collectioncollection.subCollection.group- Access collection group
Configuration Options
zodErrorHandler- Custom error handling for validation failuressnapshotDataConverter- Transform document data before validationincludeDocumentIdForZod- Include document ID in Zod validationreadonlyDocuments- Mark collection as read-onlysingleDocumentKey- Create single-document sub-collections
Operation Options
transaction- Run operation within a transactionbatch- Add operation to a batch writelastUpdateTime- Precondition check for updates_createTime/_updateTime- Include metadata in results
Firebase Admin vs Web SDK
This package is designed for server-side applications using the Firebase Admin SDK. Key differences from the web SDK version (zod-firebase):
- Server Environment: Runs in Node.js with admin privileges
- No Authentication: Admin SDK bypasses Firebase Auth
- Transactions: Full transaction support with preconditions
- Batch Operations: Atomic batch writes
- Server Timestamps: Access to server-side timestamp operations
- Metadata Access: Read/write times and other document metadata
For client-side applications, use zod-firebase instead.
License
MIT
Contributing
See the main repository at valian-ca/zod-firebase-admin for contributing guidelines.
