| name | kilncast |
| description | Read and write Firestore through kilncast, a schema-first typed wrapper over both Firestore SDKs (firebase-admin and firebase web) from one Standard Schema definition. Use when a project imports "kilncast", when writing Firestore data-access code in such a project, or when the user mentions kilncast, typed Firestore collections, or schema-typed Firestore queries. |
kilncast
Thin typed wrapper over Firestore. It types and coerces, it does not validate: the schema is used for type inference only and is never executed, so there is no runtime validation. Underneath it is plain Firestore; every handle exposes .ref to drop to the raw SDK.
Setup
npm install kilncast firebase-admin
npm install kilncast firebase
Three entrypoints:
kilncast: neutral. Schema definition, sentinels, types, KilncastError. Imports no Firebase, safe in shared modules and frontend bundles.
kilncast/admin: createDatabase(getFirestore()) over firebase-admin, plus typed ref helpers. Re-exports the whole neutral core.
kilncast/web: the same over the modular firebase SDK.
Quick start
import { collection, increment } from "kilncast";
import { createDatabase } from "kilncast/admin";
import { getFirestore } from "firebase-admin/firestore";
import { z } from "zod";
const posts = collection(
"posts",
z.object({ title: z.string(), likes: z.number(), createdAt: z.date() }),
);
const db = createDatabase(getFirestore());
await db
.collection(posts)
.set("hello", { title: "Hello", likes: 0, createdAt: new Date() });
const post = await db.collection(posts).get("hello");
await db.collection(posts).update("hello", { likes: increment(1) });
const popular = await db
.collection(posts)
.where("likes", ">=", 10)
.orderBy("likes", "desc")
.limit(10)
.get();
The schema is any Standard Schema validator (Zod, Valibot, ArkType). It describes one document's fields, excluding the id.
Core rules
- Ids are path-only. Never declare an
id field in a schema (compile error). Reads merge id in flat; writes exclude it. add(data) returns the generated id.
- Missing reads are
null. get() returns Doc<S> | null; there is no .exists snapshot to check (use .doc(id).exists() for a boolean).
- Values coerce at the boundary. Write a
Date, read a Date (stored as Timestamp); write a Uint8Array, read a Uint8Array (stored as SDK bytes). Deep through maps and arrays.
- Sentinels are
kilncast's own. Import serverTimestamp, increment, arrayUnion, arrayRemove, deleteField from kilncast, never FieldValue from an SDK. Each is type-constrained to the fields it fits (increment on numbers, deleteField on optional fields only).
- Queries are typed to the schema.
where / orderBy take typed dotted paths into nested maps ("customer.address.city"); a misspelt field or wrong value type is a compile error. Target the id with documentId() from kilncast.
- Dotted-path keys work in
update only. update("o1", { "totals.items": increment(1) }) touches one nested field. In a merge set a dotted key is a literal field name, so nest an object instead.
- Transaction and batch writes take no
await. Reads in a transaction are async; writes return void and buffer until commit. Batches are write-only (no get, no add) and apply atomically on .commit().
- No runtime validation. A document drifted from its schema comes back typed as valid. Where drift matters, run the schema on the result yourself.
- SDK semantics win.
kilncast never wraps Firestore, network, or permission errors; KilncastError covers only kilncast's own failures. Anything kilncast does not wrap is reachable via .ref.
Common operations
const col = db.collection(posts);
await col.get("hello");
await col.doc("hello").exists();
await col.set("hello", { title: "Hi", likes: 0, createdAt: new Date() });
await col.set("hello", { likes: 1 }, { merge: true });
await col.update("hello", { likes: increment(1) });
const id = await col.add({ title: "New", likes: 0, createdAt: new Date() });
await col.delete(id);
await col.where("likes", ">=", 10).orderBy("likes", "desc").limit(5).get();
await col.count();
await col.sum("likes");
await col.average("likes");
const unsub = col.onSnapshot((docs) => {});
db.collection(posts).doc("hello").collection(comments);
await db.collectionGroup(comments).orderBy("createdAt").get();
await db.runTransaction(async (tx) => {
const post = await tx.collection(posts).get("hello");
if (post) tx.collection(posts).update("hello", { likes: post.likes + 1 });
});
const batch = db.batch();
batch.collection(posts).update("hello", { likes: increment(1) });
await batch.commit();
Full reference
See REFERENCE.md for queries and cursors, sentinels and nested updates, live updates, transactions and batches in detail, raw fields and full-precision timestamps, bytes, neutral value types, the .ref escape hatch, and gotchas.