Core Concepts
The foundational building blocks of FlintFire: the per-collection repository, its four generic types, and delete semantics.
FlintFire’s core is a per-collection repository. This page covers the repository pattern and delete behavior. The other foundational topics each have their own page: document identity, schema validation, read converters, field-value sentinels, timestamps, lifecycle hooks, queries, and vector search.
Repository Pattern
Section titled “Repository Pattern”The repository abstracts Firestore operations behind a clean, consistent API. Each collection gets its own repository instance.
// Initialize once, use everywhereconst userRepo = FirestoreRepository.withSchema(db, 'users', userSchema);const orderRepo = FirestoreRepository.withSchema(db, 'orders', orderSchema);const productRepo = FirestoreRepository.raw<Product>(db, 'products'); // Without validationThe withSchema factory attaches a Zod schema for runtime validation. Both the read and write types
are inferred from schema values: the read type is z.output<readSchema>, and the write type is
z.input<writeSchema> when you pass a writeSchema overlay (otherwise it equals the read type). Do
not pass an explicit read-type generic. No readSchema (userSchema, orderSchema above) may
declare a top-level id field — the factory throws at construction if one is present. The document
name is the sole source of id, and reads resolve to FirestoreDocument<T>. A writeSchema built
from the write combinators enables cast-free combinator writes. Construct a repository directly with
FirestoreRepository.raw<Product>(db, 'products') when you don’t need validation — the named
factory keeps security-relevant options such as allowLegacyDatastoreIds discoverable instead of
trailing positional arguments. See
schema validation for the full contract.
To add domain helpers (findByEmail, deactivate, and so on), subclass FirestoreRepository or
wrap a withSchema instance — both are supported. Subclasses should wire validation with
FirestoreRepository.withSchemaArgs(...) (spread into super), because withSchema always returns
a plain repository. See
Custom repository methods for
the full contract.
The full constructor signature is
new FirestoreRepository<T, W = T, S = T, WO = W>(db, collectionPath, validator?, parentPath?, readConverter?, schemas?, allowLegacyDatastoreIds?),
where T = z.output<readSchema> (read data), W = z.input<writeSchema> (write input), S =
z.output<storedSchema> (at-rest shape, the source of query field paths), and WO =
z.output<writeSchema> (parsed write data). There is no options, config, debug, or logger bag —
everything is passed through these positional arguments (plus the trailing options object the
withSchema and subcollection factories accept: writeSchema, storedSchema, readConverter,
sentinelPolicy, and allowLegacyDatastoreIds — storedSchema is required when readConverter is
set).
Converters and schema drift
Section titled “Converters and schema drift”Reads can be customized with an optional readConverter — for example mapping a stored
Timestamp to a Date or a millisecond number on the way out. Converters are read-only: the
mapper runs on every read, the document id is overlaid afterward, and a storedSchema is required
whenever one is set. See Read Converters for the
full contract.
Because the converter runs on every read, it is also the seam for normalizing documents written under an older schema into the current shape — without a data migration. See Schema Evolution for that pattern.
Delete Behavior
Section titled “Delete Behavior”Deletes are explicit hard deletes. Calling delete() removes the document from Firestore
immediately.
await userRepo.delete('user-123');A few details worth knowing:
delete(id)throwsNotFoundErrorif the document does not exist.- Delete lifecycle hooks (
beforeDelete/afterDelete) receive the full persisted document (FirestoreDocument<T>) at runtime, so a hook can inspect what is being removed. bulkDelete(ids)resolves to the count of documents that actually existed — not the length of the input array — so ids that were already absent are not counted.