The current command and API surface. See contract.md for the annotations and projection rules, and compatibility.md for the gate.
from datetime import datetime
from enum import StrEnum
from typing import Annotated
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
from firepact import (
FirestoreBackfilled,
FirestoreRef,
FirestoreServerTimestamp,
firestore_realtime,
)
class CamelModel(BaseModel):
# by_alias=True + to_camel MUST match the backend's write serialization.
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
@firestore_realtime(collection="rooms/{roomId}/messages", id_field="id")
class Message(CamelModel):
id: str
author: Annotated[str, FirestoreRef("Profile")]
body: Annotated[str, FirestoreBackfilled()]
created_at: Annotated[datetime, FirestoreServerTimestamp()]
kind: MessageKind # a StrEnum, defined above
...Only models decorated with @firestore_realtime are roots; their transitive
closure (nested models, enums) is included automatically. A full worked example
is in ../examples/gen/chat/.
CLI (imports the module, builds the bundle, emits TS):
firepact-gen --module pkg.models --output types.ts # native entry point
pydantic2ts --module pkg.models --output types.ts # prior-tool-compatible alias--module is resolved against the current working directory. --output is
optional (defaults to stdout). --exclude is accepted for prior-tool
compatibility (no-op).
A project usually also has plain HTTP DTOs whose datetime is an ISO string
(not a Firestore Timestamp). Generate those with --plain, then have the
Firestore output import the types they share (e.g. enums) from the DTO file so
each type is defined exactly once:
# 1. plain DTOs (datetime -> string, one interface per model) -- the single source
firepact-gen --plain --module pkg.dtos --output dtos.ts
# 2. Firestore docs, importing shared types from ./dtos (names DERIVED from the
# dtos module -- no hand-maintained list; only referenced ones are imported)
firepact-gen --module pkg.models --output firestore.ts \
--shared ./dtos --shared-from pkg.dtos--shared <module-specifier> is resolved relative to --output; --shared-from
derives the shared names from the plain module's own output (so they are
guaranteed to exist there). Only enums are auto-shared -- they are
context-independent, whereas an object can be dual-context (a datetime is
Timestamp in Firestore but string in the DTO), so it keeps its own Firestore
definition. Share a pure object explicitly with --shared-names A,B. The
../examples/gen/chat/ example uses exactly this layout.
Python API:
from firepact import generate_typescript_defs
ts = generate_typescript_defs("pkg.models", output="types.ts")Low-level (bundle then Rust core):
firepact emit pkg.bundle.json # or: cat pkg.bundle.json | firepact emit -The emitter produces, per realtime root Message:
Message- the read view (onSnapshot/getDoc).MessageWrite- the write view (setDoc, create payload).MessageUpdate=UpdateData<MessageWrite>- the update view (updateDoc): optional fields,FieldValue(e.g.increment(),serverTimestamp()), and nested dotted paths.messageConverter- a read-orientedFirestoreDataConverter<Message>that injects the document id on read and strips it on write. Reads go through it; writes useMessageWritedirectly (FirestoreDataConverter has a single app type, so the read/write asymmetry cannot both be expressed through it).messagesPath(roomId)- a typed path builder from the collection template.
import { messageConverter, messagesPath, type MessageWrite } from "./types";
const ref = doc(db, `${messagesPath(roomId)}/m1`).withConverter(messageConverter);
onSnapshot(ref, (snap) => {
const m = snap.data(); // Message, with m.id populated
});
const payload: MessageWrite = { /* createdAt: serverTimestamp(), ... */ };
await setDoc(doc(db, `${messagesPath(roomId)}/m1`), payload); // write view, no converterExport the bundle, then diff it against the committed history:
firepact-gen --module pkg.models --bundle-out schemas/pkg.v2.json
firepact-compat --history schemas --new schemas/pkg.v2.json # pip-installed, native
# (equivalently, the cargo binary: firepact compat --history schemas --new ...)Both forms exist: firepact-compat (Python console script, ships in the wheel)
and firepact compat (the Rust binary). Pairwise <old.json> <new.json> also
works. Exit code is non-zero on any breaking change. firepact compat --json <old> <new> prints the findings as a JSON array on stdout (the same shape the
native module returns -- the py/rust parity tests assert the two match byte for
byte). See compatibility.md.
just lists all tasks. Common ones: just build, just test, just lint,
just test-e2e (needs the Firestore emulator + bun), just example-gen (regenerate
examples/gen/chat/generated.ts).