Strongly typed builder for writing Firebase Firestore Security Rules in TypeScript.
The goal of this package is to help you author Firestore rules with a strongly typed developer experience.
It does not model or manage your runtime database. The schema types you provide are only used to drive compile-time typing for rule context properties, helper signatures, and expression methods.
pnpm add firestore-rules-dsl- A root builder via
createAstRulesBuilder(...). - Typed
matchcallbacks viamatches(...). - Typed rule declarations via
allow(...). - A typed context (
$) withrequest,resource,request.resource,params,db, and helper methods. - Reusable helper libraries via
defineFirestoreRulesLibrary(...). - Deterministic rules output via
toString().
import {
createAstRulesBuilder,
defineFirestoreRulesLibrary,
type CollectionShape,
type DatabaseDefinition,
} from "firestore-rules-dsl"
type UserDoc = {
displayName: string
email: string
createdAt: number
}
type OrgDoc = {
name: string
ownerId: string
plan: "free" | "pro" | "enterprise"
}
type AppClaims = {
admin: boolean
orgId: string
}
type AppDb = DatabaseDefinition<
{
users: CollectionShape<UserDoc>
orgs: CollectionShape<OrgDoc>
},
AppClaims
>
const authHelpers = defineFirestoreRulesLibrary((ctx, register) => {
const isSignedIn = register("isSignedIn", [], () => ctx.request.auth.uid.is("string"))
const isOwner = register("isOwner", ["ownerId"], (_helperCtx, { ownerId }) => {
return ctx.request.auth.uid.eq(ownerId)
})
return { isSignedIn, isOwner }
})
const builder = createAstRulesBuilder<AppDb>()
.withHelpers(authHelpers)
.withHelpers((ctx, register) => {
const isAdmin = register("isAdmin", [], () => ctx.request.auth.token.admin.eq(true))
return { isAdmin }
})
builder.matches((match) => {
match("users/{userId}", (users, $) => {
users.allow("read", $.or($.isOwner($.params.userId), $.isSignedIn()))
users.allow(
"create",
$.and($.isSignedIn(), $.request.resource.data.createdAt.eq($.request.time)),
)
users.allow("update", $.isOwner($.params.userId))
users.allow("delete", $.or($.isOwner($.params.userId), $.isAdmin()))
})
})
const rulesSource = builder.toString()
console.log(rulesSource)Use DatabaseDefinition<TCollections, TCustomClaims> and CollectionShape<TDoc, TSubcollections> to provide type information for the rule context.
type AppDb = DatabaseDefinition<
{
orgs: CollectionShape<
{ name: string; ownerId: string },
{
projects: CollectionShape<{ name: string; ownerId: string }>
}
>
},
{ admin: boolean }
>These types are compile-time only and exist to power autocomplete, inference, and safety in your rules code.
createAstRulesBuilder<Db>() creates the root builder.
You can then:
- call
withHelpers(...)to register helper libraries, - call
matches(...)to define path scopes, - call
allow(...)on each match builder, - call
toString()to render Firestore rules text.
Use matches(...) to define nested match scopes.
builder.matches((match) => {
match("orgs/{orgId}", (orgs, $) => {
orgs.allow("read", $.request.auth.uid.is("string"))
orgs.matches((match) => {
match("projects/{projectId}", (projects, $) => {
projects.allow("read", $.resource.data.ownerId.eq($.request.auth.uid))
})
})
})
})Use allow(operation, condition) to attach a rule condition to one or more operations.
users.allow("read", $.request.auth.uid.is("string"))
users.allow("update", $.resource.data.ownerId.eq($.request.auth.uid))
users.allow(["create", "delete"], $.request.auth.token.admin.eq(true))Use defineFirestoreRulesLibrary(...) to publish reusable helper libraries with full typing preserved.
import { defineFirestoreRulesLibrary } from "firestore-rules-dsl"
export const sharedHelpers = defineFirestoreRulesLibrary((ctx, register) => {
const isSignedIn = register("isSignedIn", [], () => ctx.request.auth.uid.is("string"))
const isAdmin = register("isAdmin", [], () => ctx.request.auth.token.admin.eq(true))
return { isSignedIn, isAdmin }
})Then attach them with withHelpers(...).
Inside each match callback, $ is strongly typed.
$.request.auth.uid$.request.auth.token.<claim>$.request.method$.request.path$.request.time$.request.resource.data(incoming document data for writes)$.resource.data(existing persisted document data)$.params.<segmentName>$.db(schema-aware traversal forexists(...)andget(...)patterns)
$.exists(path)$.get(path)$.getAfter(path)$.and(...conditions)$.or(...conditions)$.not(condition)$.op(left, operator, right)$.duration.abs(...),$.duration.time(...),$.duration.value(...)$.hashing.md5(...),$.hashing.sha256(...)$.math.abs(...),$.math.ceil(...),$.math.floor(...),$.math.pow(...),$.math.round(...),$.math.sqrt(...)
Comparison and arithmetic:
.is(...).eq(...),.neq(...),.gt(...),.gte(...),.lt(...),.lte(...).plus(...),.minus(...),.multiply(...),.divide(...),.modulo(...)
List methods:
.size().hasAll(...),.hasAny(...),.hasOnly(...).join(...),.concat(...),.removeAll(...),.toSet()
Map methods:
.size().keys().diff(...)with.addedKeys(),.removedKeys(),.changedKeys(),.affectedKeys(),.unchangedKeys()
If your schema is already defined with Typesaurus, use createTypesaurusRulesBuilder(...) from the typesaurus subpath.
import { schema } from "typesaurus"
import { createTypesaurusRulesBuilder } from "firestore-rules-dsl/typesaurus"
const db = schema(($) => ({
users: $.collection<{ name: string }>(),
}))
const builder = createTypesaurusRulesBuilder(db).withCustomClaims<{
admin: boolean
orgId: string
}>()
builder.matches((match) => {
match("users/{userId}", (users, $) => {
users.allow("read", $.request.auth.token.admin)
})
})The backward-compatible OfTypesaurus alias is also exported for existing codebases.
firestore-rules-dslfirestore-rules-dsl/astfirestore-rules-dsl/builderfirestore-rules-dsl/libraryfirestore-rules-dsl/testingfirestore-rules-dsl/typesaurus
If you are migrating from earlier versions:
createFirestoreRulesBuilder(...)was replaced bycreateAstRulesBuilder(...).- rule definition moved to
matches(...)plusallow(...). - helper libraries should be authored with
defineFirestoreRulesLibrary(...).
pnpm run build
pnpm run lint
pnpm run typecheck
pnpm run testMIT
Made with 💖 in TypeScript by Huafu from Thailand.