An open API service indexing awesome lists of open source software.

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.

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: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![version](https://img.shields.io/badge/version-0.0.15-orange.svg)](CHANGELOG.md)
[![Dart](https://img.shields.io/badge/Dart-%5E3.5-0175C2?logo=dart&logoColor=white)](https://dart.dev)
[![tests](https://img.shields.io/badge/tests-62_passing-brightgreen.svg)](test/)
[![status](https://img.shields.io/badge/status-pre--pub-yellow.svg)](#-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.