https://github.com/moinsen-dev/firepack
Spec-driven Firestore + Flutter codegen. Define your collections, fields, indexes, and rules in one YAML file. Get back typed Dart models, Riverpod providers, repositories, security rules, indexes, TypeScript types, and a visual data-model graph β regenerated on every save.
https://github.com/moinsen-dev/firepack
code-generation dart firebase flutter orm
Last synced: about 1 month ago
JSON representation
Spec-driven Firestore + Flutter codegen. Define your collections, fields, indexes, and rules in one YAML file. Get back typed Dart models, Riverpod providers, repositories, security rules, indexes, TypeScript types, and a visual data-model graph β regenerated on every save.
- Host: GitHub
- URL: https://github.com/moinsen-dev/firepack
- Owner: moinsen-dev
- License: mit
- Created: 2026-04-28T09:05:29.000Z (2 months ago)
- Default Branch: develop
- Last Pushed: 2026-04-29T16:37:06.000Z (about 2 months ago)
- Last Synced: 2026-04-29T18:24:04.128Z (about 2 months ago)
- Topics: code-generation, dart, firebase, flutter, orm
- Language: Dart
- Homepage: https://moinsen-dev.github.io/firepack/
- Size: 486 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Roadmap: docs/ROADMAP.md
Awesome Lists containing this project
README
# π₯ firepack
### One YAML spec β your entire Firestore data layer.
**Stop hand-writing the 9-file fan-out for every new field.**
Typed Dart models, Riverpod providers, repositories, security rules,
indexes, storage paths, and a Mermaid graph β generated deterministically,
on every save.
[](LICENSE)
[](CHANGELOG.md)
[](https://dart.dev)
[](test/)
[](#-status)
[**What it generates**](#-what-firepack-generates) Β·
[**Quick start**](#-quick-start) Β·
[**Why**](#-why-firepack-exists) Β·
[**Spec reference**](docs/SPEC.md) Β·
[**Roadmap**](docs/ROADMAP.md)
---
## β¨ The 30-second pitch
You write **one** YAML file:
```yaml
firepack: 1
project: blog
collections:
posts:
tenant: organizationId
fields:
id: { type: string, primaryKey: true }
title: { type: string, required: true }
status: { type: "enum[PostStatus]", default: draft }
createdAt: { type: dateTime, required: true, serverDefault: now }
organizationId: { type: "ref[organizations.id]", required: true }
indexes:
- fields: [organizationId, status, createdAt:desc]
queries:
watchByOrg:
where: [tenant]
orderBy: createdAt:desc
limit: 50
```
You get **all of this** back, every save:
```
your-app/
ββ lib/firepack/
β ββ paths.dart # FirestorePaths.posts ...
β ββ firestore_provider.dart # one Riverpod DI seam β override once for tests
β ββ models/post.dart # immutable + copyWith + ==/hashCode +
β β toJson/fromJson + toFirestore/fromFirestore
β ββ repositories/post_repository.dart # typed queries + add(merge:) / update / delete
β ββ storage_paths.dart # typed Cloud Storage path helpers
ββ firestore.rules # generated, with reusable rule helpers
ββ firestore.indexes.json # composite indexes from spec
ββ DATA_MODEL.md # Mermaid ER diagram, GitHub-renderable
```
Edit the spec β `firepack watch` regenerates everything β your Flutter app
recompiles against the new shape. One source of truth, zero hand-typing.
---
## π What firepack generates
| Target | Output | Highlights |
|---|---|---|
| π§± **Dart models** | `lib/firepack/models/*.dart` | Immutable, value-equal, `copyWith`, `toJson`/`fromJson` (pure JSON) **+** `toFirestore`/`fromFirestore` (DateTime β Timestamp), enums with snake_case wire-format, nested types |
| π **Repositories** | `lib/firepack/repositories/*.dart` | Typed `Stream>` per spec query, `add(doc, {merge:})`, `updateById`, `deleteById`, `watchById`. `serverDefault: now` injects `FieldValue.serverTimestamp()` |
| π― **Riverpod providers** | inline in each repo file | `StreamProvider.family` per query, single `firestoreProvider` DI seam β override once for fakes |
| π‘οΈ **Firestore rules** | `firestore.rules` | Per-collection read/create/update/delete; reusable helpers (`isSignedIn`, `isAdmin`, `isSupervisorOrAdmin`, tenant scope); deny-all default |
| ποΈ **Composite indexes** | `firestore.indexes.json` | Sorted, deterministic, dedup'd β diffs cleanly in PRs |
| π **Storage paths** | `lib/firepack/storage_paths.dart` | Typed methods per declared bucket β drift between upload/read/rules code becomes impossible |
| πΊοΈ **Path constants** | `lib/firepack/paths.dart` | `FirestorePaths.` β rename a collection in the spec, the build (not the runtime) breaks |
| π **Mermaid graph** | `DATA_MODEL.md` | ER diagram with FKs, nested types, storage-bucket cross-system edges. Renders inline on GitHub |
| π **Spec diff** | Markdown report | PR-comment-shape diff between two specs (added/removed/changed) |
Everything is **deterministic**: two runs on the same spec produce
byte-identical output. Snapshot tests catch any drift in a generator.
---
## π The data model, visualised
The example spec renders to this β generated by `firepack viz`,
re-rendered on every spec save:
```mermaid
erDiagram
organizations {
string id "PK"
string name "required"
datetime createdAt "required"
}
users {
string id "PK"
ref organizationId "FK"
string displayName "required"
string email "required"
enum role
}
posts {
string id "PK"
ref organizationId "FK"
ref authorId "FK"
string title "required"
string body "required"
string coverImage "storage"
list attachments
type linkPreview
enum status
datetime createdAt "required"
datetime publishedAt "optional"
}
comments {
string id "PK"
ref organizationId "FK"
ref postId "FK"
ref authorId "FK"
string body "required"
datetime createdAt "required"
}
postCovers {
string path "covers/{organizationId}/{postId}.jpg"
}
attachments_bucket {
string path "attachments/{organizationId}/{postId}/{attachmentId}"
}
users }o--|| organizations : "organizationId"
posts }o--|| organizations : "organizationId"
posts }o--|| users : "authorId"
posts }o--|| postCovers : "coverImage (storage)"
comments }o--|| organizations : "organizationId"
comments }o--|| posts : "postId"
comments }o--|| users : "authorId"
```
Storage buckets, foreign keys, nested types β all in one diagram, all
generated. No hand-drawn ER stays in sync with code; this one does.
---
## π Quick start
The bundled [`example/`](example/) is a runnable Flutter + Riverpod
app consuming firepack-generated code. Five commands from clone to
running app:
```bash
# 1. Install firepack from this clone (until pub.dev release)
git clone https://github.com/moinsen-dev/firepack && cd firepack
dart pub global activate --source path .
export PATH="$PATH:$HOME/.pub-cache/bin" # add to ~/.zshrc
# 2. Sanity check the toolchain on the example spec
firepack lint --spec example/firepack.yaml
# 3. Regenerate the example app's data layer
just example-regen
# 4. Boot the local Firebase Emulator (Firestore + Auth + UI)
just example-emulator-up # in one terminal
# 5. Run the Flutter app against the emulator
just example-run # in another
```
Open the app β **Seed demo data** β 3 posts appear, each created via
the generated `PostRepository.add()`, streamed live through the
generated Riverpod provider. Click a tile to edit (β `updateById`),
trash icon to delete (β `deleteById`). Watch the writes land in
the Emulator UI at .
> **Prerequisites**: Flutter β₯ 3.24, JDK 21+ for the Firestore emulator
> (the justfile auto-picks `brew install openjdk@21` if present, no
> system-wide JDK switch needed).
---
## π οΈ Why firepack exists
Adding one field to a Firestore-backed Flutter app fans out across **nine files**:
1. Dart model class
2. `freezed` annotation + generated part
3. `json_serializable` part
4. Repository (read path)
5. Repository (write path)
6. Riverpod provider
7. Firestore security rule
8. Composite index
9. UI form / display
The fan-out is **mechanical and error-prone**. Half the production
incidents come from rule and index drift β fixed in one file, missed
in another.
firepack collapses the fan-out to **one diff in `firepack.yaml`**.
Re-run, build, ship.
It's not magic. It's a small Dart CLI that reads YAML and writes
Dart/JSON/text β deterministically, with snapshot tests proving every
output. The spec format is intentionally minimal (~150 LoC of parser,
no DSL fanciness). Less surface to maintain, more leverage per change.
---
## π Compared to the alternatives
| | **firepack** | `freezed` + `json_serializable` | Hand-written |
|---|---|---|---|
| Source of truth | YAML spec | Dart class | scattered |
| Rules + indexes generated? | β
from same spec | β separate | β separate |
| Riverpod providers generated? | β
| β (write yourself) | β |
| Storage paths typed? | β
from `storage:` block | β | β |
| Watch / hot-regen? | β
`firepack watch` | β (build_runner) | β |
| Mermaid data-model graph? | β
with cross-system edges | β | β |
| Spec diff for PR review? | β
`firepack diff` | β | β |
| Build-runner needed? | β pure Dart, watcher | β
| n/a |
| New-collection cost | edit YAML, regen | new files in 5 places | new files in 9 places |
firepack is **not** a replacement for `freezed` everywhere β if your
classes are pure Dart with sealed unions and complex pattern matching,
keep using freezed. firepack solves the specific shape of "Firestore
collection β Flutter UI β Cloud Functions" plumbing.
---
## π CLI reference
| Command | Effect |
|---|---|
| `firepack lint --spec ` | validates spec (orphan refs, storage refs, missing tenants, name collisions) |
| `firepack viz --spec [--out file.md]` | renders Mermaid `erDiagram`; `.md` wraps in code fence for GitHub render |
| `firepack regen --target --spec [--out ]` | one-shot codegen. Targets: `models`, `repos`, `paths`, `firestore_provider`, `storage`, `rules`, `indexes` |
| `firepack watch --spec [--config ]` | first-pass regen + watch spec, re-runs on save. Targets in `firepack.config.yaml` |
| `firepack diff --old --new ` | semantic spec diff (PR-comment shape) |
Run `firepack --help` for full option lists.
---
## π¦ Install
**Pre-pub-dev release**, install from a local clone:
```bash
git clone https://github.com/moinsen-dev/firepack
cd firepack
just install # = dart pub global activate --source path .
```
Make sure `~/.pub-cache/bin` is in your PATH:
```bash
export PATH="$PATH:$HOME/.pub-cache/bin" # add to ~/.zshrc or ~/.bashrc
firepack --help # verify
```
Path-source activations track the working tree β after `git pull`
the binary picks up changes immediately. No need to re-activate.
---
## π§ͺ Status
**Working today (v0.0.15):**
- β
YAML spec parser + lint
- β
Mermaid `firepack viz` output (`.md` auto-wraps in code fence)
- β
Composite indexes generator (deterministic)
- β
Firestore rules generator (with reusable helpers + deny-all default)
- β
Dart model codegen β immutable class with `copyWith`, `==`/`hashCode`,
pure-JSON `toJson`/`fromJson` **and** Firestore-native
`toFirestore`/`fromFirestore` (handles `Timestamp` β `DateTime`
via duck-typing)
- β
Repository + Riverpod provider codegen β typed queries from spec,
`add(doc, {merge:})`, `updateById`, `deleteById`, `watchById`
- β
Per-collection `className:` override
- β
Single `firestoreProvider` DI seam β override once for fakes
- β
`serverDefault: now` β `FieldValue.serverTimestamp()` injection
- β
Storage refs + typed `StoragePaths.()` helpers
- β
`firepack diff` (PR-comment-shape semantic diff)
- β
`firepack watch` (auto-regen on save, multi-target via config)
- β
Shared enums with `snake_case` / `dartName` wireFormat
- β TypeScript-types generator (`functions/src/firepack/types/*.ts`)
- β Pub.dev release (waiting for spec format to stabilise)
- β Transactions / batch-writes codegen
- β CI for firepack itself
**Real-world consumption:** the [WorkBrief](https://workbrief.app)
production app uses firepack as its data-layer generator since v0.0.2.
13 collections, 3 nested types, 2 storage buckets β generated from
one spec, regenerated on every change.
See the [CHANGELOG](CHANGELOG.md) for milestone history.
---
## πΊοΈ Project structure
```
firepack/
ββ bin/firepack.dart # CLI entry point (CommandRunner)
ββ lib/
β ββ firepack.dart # public exports
β ββ src/
β ββ spec/ # parser, model, lint
β ββ codegen/ # one generator per target
β ββ diff/ # spec diff
β ββ viz/ # Mermaid renderer
ββ test/ # generator snapshot tests + parser tests
ββ example/ # runnable Flutter + Riverpod demo app
ββ docs/
ββ PHILOSOPHY.md # bootstrap principle + Non-Goals
ββ ROADMAP.md # milestone history with reflections
ββ SPEC.md # formal spec reference (v1)
```
`docs/PHILOSOPHY.md` is worth reading first if you're considering
contributing β it explains the bootstrap rule (every feature starts
from a real consumer pain) and the deliberate Non-Goals list.
---
## π€ Contributing
firepack is **bootstrap-driven**: features land when they solve a real
consumer pain, not speculatively. If you have a use-case that the
current spec doesn't cover, open an issue describing the shape of code
you're hand-writing today. The fix is usually a new spec field
+ a generator branch + a snapshot test.
Local development:
```bash
just check # fmt-check + analyze + dart test (62) + flutter analyze
just test # just dart test
just example-regen # regen the example app's generated tree
```
---
## π License
MIT β see [LICENSE](./LICENSE).
---
Built with care by [@moinsen-dev](https://github.com/moinsen-dev) Β·
π if firepack saves you a fan-out, star the repo so others find it.