Getting Started
Firemap is a type-safe Firestore ODM that uses TypeScript decorators to define schemas, validate data at runtime, and generate infrastructure (Cloud Functions, indexes, security rules).
The reason to want one is that Firestore gives you no schema at all. Documents in a collection can hold any fields of any types; a snapshot comes back as an untyped bag of values; and the three files that describe your data outside the database — security rules, composite indexes, and the Cloud Functions that keep denormalized copies current — are maintained separately from the code that reads and writes it. Nothing keeps those four descriptions in agreement, and Firestore has no migrations, so drift is discovered in production.
Installation
npm install @firemap/sdkThe SDK sits on top of whichever Firestore client you already use — it works against both the web client SDK and the Admin SDK, and does not replace them. Call initializeFiremap(db) once at startup with your existing Firestore instance, passing a second argument of true when that instance came from firebase-admin. Queries issued before that call throw a NOT_INITIALIZED error rather than silently doing nothing.
Quick Start
import {
Collection, Field, Required,
AuthRequired, BaseModel
} from '@firemap/sdk';
@AuthRequired
@Collection('users')
class User extends BaseModel {
@Required
@Field({ type: 'string' })
name!: string;
@Required
@Field({ type: 'string' })
email!: string;
@Field({ type: 'timestamp' })
createdAt!: Date;
}
// Type-safe queries
const user = await User.findById('abc123');
const active = await User.find({
where: { status: 'active' },
orderBy: 'createdAt',
limit: 10,
});
// Validated writes
await User.create({
name: 'John',
email: '[email protected]',
});Three things are happening in that class. @Collection('users') binds the class to a Firestore collection path and registers it, which is what makes the static query methods on BaseModel know where to look. @Field records the Firestore type of each property, which is separate information from the TypeScript type — TypeScript is erased at build time and cannot check a value that arrived over the network at runtime. And @AuthRequired is a security rule declaration: it does nothing at runtime, but the CLI turns it into a match block in your generated rules file.
On the read side, findById resolves to User | null and find to User[], with the keys of where constrained to fields that exist on the model — a typo in a filter is a compile error rather than an empty result set. On the write side, create fills in declared defaults, checks that every @Required field is present, and validates each value against its declared type before the call reaches Firestore, throwing a FiremapValidationError that names the offending collection and field.
TypeScript Config
Firemap requires TypeScript 5.0+ with Stage 3 decorators:
{
"compilerOptions": {
"target": "ES2022",
"experimentalDecorators": false,
"emitDecoratorMetadata": false
}
}Those two false values are the part people get wrong. TypeScript has shipped two unrelated decorator implementations: the old experimentalDecorators flavour, still used by most older libraries, and the TC39 Stage 3 standard implemented in TypeScript 5.0. They pass different arguments to the decorator function and are not interchangeable, so leaving experimentalDecorators on — which many project templates and NestJS-derived configs do — makes the decorators fail in ways that look like the metadata simply never registered. emitDecoratorMetadata only exists for the legacy flavour and has no effect here, which is why field types are declared explicitly on @Field rather than inferred.
From here, the decorator reference covers field types, defaults and soft deletes, the model API covers queries and realtime streams, and the CLI turns all of it into deployable Firebase config.