Keep your Pydantic backend and your TypeScript frontend agreeing on the
wire shape of Firestore Native-mode documents read in realtime via
onSnapshot — and fail CI on a schema change that would break a frontend still
reading the old shape (FULL_TRANSITIVE).
firepact is not just a type converter. It generates the TypeScript types your frontend imports and runs a compatibility gate over the contract as it evolves. Before you rely on the green check, read what firepact is — and is not: it gates the evolution of the contract, not the data already sitting in Firestore.
pip install firepact # Python CLI (firepact-gen / firepact-compat) + native engine
cargo install firepact-core # standalone Rust binary `firepact` (no Python/Node)1. Mark the models you read in realtime. The decorator records the collection path and which fields are guaranteed on every document; the backend writes with a camelCase alias generator (firepact matches it).
from datetime import datetime
from typing import Annotated
from firepact import firestore_realtime, FirestoreServerTimestamp
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
class CamelModel(BaseModel): # camelCase wire keys
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@firestore_realtime(collection="rooms/{roomId}/messages", guaranteed=["body"])
class Message(CamelModel):
id: str # document id
body: str
created_at: Annotated[datetime, FirestoreServerTimestamp()]
tags: list[str] = []2. Generate the TypeScript your frontend imports.
firepact-gen --module app.models --output src/firestore.ts// @firestore-collection rooms/{roomId}/messages
export interface Message { // read view, for onSnapshot()
body: string; // guaranteed -> required even on old docs
createdAt?: Timestamp | null; // server timestamp: null until it resolves
id: string; // the converter injects snapshot.id
tags?: string[]; // not guaranteed -> optional (safe default)
}
export interface MessageWrite { // write view, for setDoc(): id is excluded
body: string;
createdAt: FieldValue; // serverTimestamp()
tags: string[];
}The full worked example (refs, open enums, discriminated unions, vectors,
GeoPoints, bytes) is in examples/gen/chat/.
3. Gate compatibility in CI. Export the contract bundle per release and diff each change against the committed history; a breaking change fails CI.
firepact-gen --module app.models --bundle-out schemas/v2.json
firepact-compat --history schemas --new schemas/v2.jsonSee usage for the read/write/update views and the converter, and the compatibility gate for what counts as breaking.
Verified in CI (see .github/workflows/ci.yaml).
| Component | Supported | Notes |
|---|---|---|
| Python | 3.11 – 3.14 | one abi3 wheel covers 3.11+ |
| Pydantic | 2.9 – 2.13 | drift canary; the exact schema golden is pinned to the locked version |
| JSON Schema | Draft 2020-12 | Pydantic's default dialect |
| TypeScript (output) | 5.x / 6.x / 7.x | type-checks under verbatimModuleSyntax + isolatedModules |
| firebase JS SDK | v11+ | Timestamp, GeoPoint, DocumentReference, Bytes, VectorValue, FieldValue, UpdateData, FirestoreDataConverter |
| Rust | 1.75+ | MSRV (Cargo.toml) |
Dependency bumps within these ranges are tracked by Dependabot.
- scope — what firepact is and is not (read this first)
- usage — annotating models, the read/write/update views, the gate
- contract & projection — the
x-firestore-*vocabulary - compatibility — the
FULL_TRANSITIVEgate and its taxonomy - architecture — the two components and the single bundle
docs/adr/— the decisions (the "Why")
firepact-core(Rust crate, binaryfirepact): pure, Python/Node-free.firepact emitprojects one enriched JSON Schema bundle into read/write/update TypeScript;firepact compatis the gate.firepact(Python package): imports your Pydantic models, delegates schema generation to Pydantic, stamps thex-firestore-*vocabulary, and emits via the native core. Console scripts:firepact-gen,firepact-compat, andpydantic2ts(a drop-in alias for the prior tool).
just build # build the Rust core + `firepact` binary
just test # all tests (rust + python)
just lint # rust + python + markdown checks
just example-gen # regenerate the generation examples (examples/gen/)
just example-compat # gate the compat example against its committed historyThe Firestore-specialised, from-scratch successor to pydantic-to-typescript (which targeted FastAPI request/response types and depended on Node). MIT licensed (LICENSE).