The worked reference for the real layered firepact setup: Firestore documents
and plain HTTP DTOs, generated into separate files with each shared enum defined
exactly once. This one uses the decorator-on-model style; see
../realtime_app/ for the production style (firepact kept out
of the model source, snake_case wire).
A Pydantic datetime maps to a different TS type depending on where the value
lives. This example shows all three:
| Field | Annotation | read view | write view |
|---|---|---|---|
Message.createdAt |
datetime + FirestoreServerTimestamp() |
Timestamp | null |
FieldValue (serverTimestamp()) |
Message.editedAt |
plain datetime (Firestore field) |
Timestamp |
Timestamp | Date |
SendMessageRequest.clientTime |
plain datetime (HTTP DTO, --plain) |
string |
string |
So on a Firestore read you get a real Timestamp (data.editedAt.toDate()),
while an HTTP payload gives you a string (new Date(clientTime)) -- the two are
never confused because they are different generated types.
models.py- the Firestore document models.Messageis a@firestore_realtimeroot;Profile/Reaction/Attachmentare its transitive closure. Wire keys are camelCase (by_alias=True+to_camel), matching the backend's write serialization.dtos.py- plain HTTP request/response DTOs (SendMessageRequest,SendMessageResponse). Theirdatetimeis an ISO string over HTTP, not a FirestoreTimestamp.MessageKindis shared withMessageand is defined here (the plain layer is the single source).
dtos.ts- generated withfirepact-gen --plain: one plain interface per model,datetime->string, strict enums. DefinesMessageKind.generated.ts- the Firestore contract (TypeScript): read/write/update views, the read converter (doc-id injection), open string enums, a typed path helper. It importsMessageKindfrom./dtos(via--shared-from) instead of redefining it, so every type is defined exactly once across the two files.bundle.json- the deterministic contract bundle (--bundle-out): the enriched JSON Schema that both the TypeScript above and the compatibility gate consume. It is the artifact a project freezes as schema history (see../../compat/).
just example
# which runs:
firepact-gen --plain --module examples.gen.chat.dtos --output examples/gen/chat/dtos.ts
firepact-gen --module examples.gen.chat.models --output examples/gen/chat/generated.ts \
--shared ./dtos --shared-from examples.gen.chat.dtos--shared-from examples.gen.chat.dtos derives the shared names from the dtos module's
own output (the types it defines), so there is no hand-maintained list and the
imported names are guaranteed to exist in dtos.ts; firepact imports only the
ones the Firestore docs actually reference.
tests/integration/test_emit_chain.py keeps both outputs from drifting.