This content originally appeared on DEV Community and was authored by
Stop hand-running Firebase CLI every time your schema changes. Wrap Data Connect codegen behind a single dev command and CI workflow that regenerates, tests, and publishes SDKs automatically.

You know that sinking feeling when your Firebase schema changes and suddenly every PR is a roulette wheel of “who forgot to run codegen this time”? It’s like playing whack-a-mole with your CLI except the mole always wins.
The good news: you don’t actually have to babysit the Firebase CLI. With a tiny wrapper, pinned tooling, and a clean CI pipeline, you can turn SDK generation into a zero-touch process. Locally, you type one command (pnpm dc:gen
). In CI, everything else just… happens. Diffs show up, tests run, packages publish. No one forgets, nothing drifts.
TLDR: pin the Firebase CLI in Docker, hash your schema to detect changes, and let GitHub Actions regenerate + publish SDKs automatically. Devs run one command, CI handles the grind.
The pain point: brittle manual cli steps
Every Firebase Data Connect project starts the same way: someone runs a CLI command, types get generated, and everything feels fine… until the schema changes. Then the chaos begins.
You know the drill:
- One teammate forgets to regen before committing.
- Another runs it locally but with a slightly different CLI version.
- Suddenly the PR diff looks like a cursed slot machine of type mismatches and half-baked SDKs.
Manual codegen feels like doing chores in a co-op game: everyone promises they’ll take out the trash, but the garbage keeps piling up because someone always forgets. And worse, even if you do remember, the environment might not match CI so your “fix” just creates more noise.
The result? Broken builds, noisy reviews, and that awkward “did you run firebase dc:codegen
?” comment chain that haunts every PR. It’s not that devs are lazy we just shouldn’t have to think about this stuff. Tooling should handle it.
Design goals: reproducible, idempotent, observable
So what do we actually want out of this whole setup? A few simple things that make dev life less painful:
1. Reproducibility
Codegen should work the same whether you run it on your laptop, in CI, or on your teammate’s slightly cursed Windows machine. That means pinning versions of the Firebase CLI, Node, and even the container environment. If it worked once, it should work again tomorrow no surprises.
2. Idempotency
If nothing changed in the schema, the codegen step should be a no-op. No random diffs, no wasted time. Think of it like running terraform plan
you only want updates when the actual source changes, not when the wind blows differently on your machine.
3. Observability
When codegen does run, the output should be crystal clear. You should see exactly what changed in the SDK so reviewers aren’t left guessing. A good system makes diffs obvious, not mysterious.
Put together, these three goals keep the whole thing boring and boring is good. Developers shouldn’t be arguing about generated files in code review.
Pinning tools & versions (a.k.a. stop playing version roulette)
One of the biggest mistakes teams make is letting Firebase CLI versions drift. You know the story: you run codegen locally, CI runs a slightly newer version, and suddenly the generated types don’t line up. Congrats you’ve just spawned an infinite diff loop.
The fix? Pin everything.
-
Firebase CLI → lock to a specific version in a
Dockerfile
. Example:
DOCKERFILE:
FROM node:20-slim
RUN npm install -g firebase-tools@13.14.2
- That way, no matter where you run it, you always get the same output.
-
Node → lock via
.nvmrc
orpackage.json → engines
. CI shouldn’t be on Node 18 while your devcontainer is on Node 20. -
Devcontainer → optional but powerful. With a
.devcontainer.json
, every dev can spin up the same environment instantly in VS Code or GitHub Codespaces.
It’s like pinning dependencies in a game modpack: if one person upgrades Skyrim mods without telling anyone, suddenly half the team is crashing to desktop. Lock it down, and everyone plays the same version.
Links worth bookmarking:

The schema fingerprint (when to trigger regen)
Here’s the trick: you don’t actually want to run codegen every time. It’s slow, noisy, and pointless if nothing changed. What you really want is a fingerprint a hash of your schema + config that tells you if regeneration is necessary.
Think of it like a save checksum in a game: if the file hasn’t changed, you don’t reload the whole world.
How it works:
- Concatenate your Data Connect schema and config files.
- Run them through a hashing function (MD5, SHA256, pick your flavor).
- Compare the new hash to the cached one.
- If it matches → skip codegen. If not → regenerate SDKs.
Example in Node (super simplified):
import { createHash } from "crypto";
import { readFileSync } from "fs";
function fingerprint(files) {
const hash = createHash("sha256");
files.forEach(f => hash.update(readFileSync(f)));
return hash.digest("hex");
}
In CI, you can store this fingerprint in the cache and only trigger codegen when it changes. Locally, it means lightning-fast dev loops because nothing runs unless it has to.
Result: no more wasted time, no more random diffs, and a dev workflow that feels snappy instead of sluggish.
The wrapper script: one entrypoint to rule them all
At this point, we’ve got pinned versions and a schema fingerprint. Now we just need a single command that ties it all together something every dev can run without thinking.
Enter the wrapper. A tiny Node/TS script that:
- Computes the schema fingerprint.
- Skips codegen if nothing changed.
- Runs Firebase Data Connect codegen if it did.
- Prints a nice, colorized diff so reviewers know exactly what changed.
That’s it. One entrypoint. No more “remember this CLI incantation” nonsense.
In package.json
:
{
"scripts": {
"dc:gen": "node scripts/dc-gen.mjs"
}
}
And inside scripts/dc-gen.mjs
:
import { execSync } from "child_process";
import { fingerprint } from "./fingerprint.js";
const oldHash = readFileSync(".schema-hash", "utf8");
const newHash = fingerprint(["firebase.schema.json", "firebase.config.json"]);
if (oldHash === newHash) {
console.log("✅ No schema changes, skipping codegen");
process.exit(0);
}
execSync("firebase dc:codegen", { stdio: "inherit" });
writeFileSync(".schema-hash", newHash);
console.log("🎉 Codegen complete. Check git diff for updates.");
From a dev’s perspective, the workflow shrinks down to:
pnpm dc:gen
That’s the entire developer experience. It’s like pressing the “easy button” for Firebase codegen.

Local dx: one command and a pre-commit guard
The real win here isn’t just the wrapper script it’s how it changes day-to-day dev life.
Instead of juggling CLI flags, devs now just run:
pnpm dc:gen
That’s it. No one has to remember weird options, and no one has to guess if they’re on the right version.
To keep the team honest, add a pre-commit hook (Husky + lint-staged works fine) that calls the wrapper before code gets pushed. If the schema changed and you forgot to regen, the hook catches it before your PR even lands.
Example .husky/pre-commit
:
#!/bin/sh
pnpm dc:gen
git add .
It’s basically a safety net. Now when you push code, you don’t wake up the next morning to “hey, CI failed because you forgot to run codegen.” Instead, the hook smacks your hand early and saves you the embarrassment.
I’ve lost count of how many Friday evenings I spent fixing builds that broke because someone (okay fine, me) didn’t regenerate types. This setup completely kills that headache. Local loops stay fast, PRs stay clean, and nobody plays the blame game.
Ci workflow: pr checks + publishing
Okay, local devs are happy. But CI is where this really shines. Instead of relying on humans, let the pipeline enforce everything.
A minimal GitHub Actions flow looks like this:
name: Firebase SDK CI
on:
pull_request:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Build Docker image
run: docker build -t dc-gen .
- name: Run wrapper
run: pnpm dc:gen
- name: Lint & typecheck
run: pnpm lint && pnpm tsc
- name: Run tests
run: pnpm test
- name: Upload artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: sdk
path: ./generated-sdk
- On PRs: CI runs the wrapper, shows diffs in generated code, and fails if someone forgot to commit updates. Reviewers get clean visibility.
- On main merges: CI can auto-commit regenerated SDKs or even publish them as a versioned package (npm, Maven, SwiftPM). One merge = fresh SDK everywhere.
The result: no drift between local and CI, no late-night “hotfix commits,” and a pipeline that feels like a teammate who always remembers the boring chores.
This is also where you can hook in optional steps like caching node_modules, Docker layers, or even schema fingerprints for turbo-speed runs.
CI isn’t just a safety net here it’s the automation glue that makes the whole “zero-touch” pitch real.

Handling multi-sdk repos (web, android, ios in one place)
If you’re only targeting web, life’s pretty chill. But most real apps these days ship across web + Android + iOS, which means multiple SDKs that all need to regenerate in sync. That’s where the wrapper pattern scales.
Monorepos to the rescue
If you’re running Nx or Turborepo, you can expose dc:gen
as a target so each SDK project (web/Android/iOS) has its own dependency graph. When the schema fingerprint changes, only the affected SDK targets rebuild. Faster pipelines, cleaner caching.
Platform hooks
- Web → just npm scripts like we’ve already shown.
- Android → wrap the wrapper (heh) as a Gradle task:
GRADLE:
task dcGen(type: Exec) {
commandLine "pnpm", "dc:gen"
}
- iOS → add it as a Swift Package plugin, so Xcode devs get the same one-command workflow.
One interface to rule them all
The beauty is you don’t have three different commands for three platforms. Everyone web, Android, iOS just runs:
pnpm dc:gen
Behind the scenes, the wrapper figures out which SDKs need updates.
It feels like playing a co-op game with proper class roles. Web, Android, and iOS devs might play different “characters,” but the wrapper keeps everyone on the same quest line.
Caching wins and common footguns
Once you’ve got the wrapper and CI wired up, caching becomes the secret sauce that makes everything fast instead of sluggish.
Caching wins
- schema fingerprint cache → skip codegen entirely when nothing changed.
- Docker layer caching → don’t rebuild Firebase CLI + Node every run.
- node_modules / pnpm store → restore deps instantly in CI instead of reinstalling.
With all three, your CI run time goes from “make a coffee” to “blink and it’s done.”
Common footguns (and how to dodge them)
- CLI mismatch: If your local Firebase CLI version drifts from the pinned Docker version, you’ll see phantom diffs. Fix = always run codegen inside the container.
-
dirty state builds: If you forget to
git add
generated files before pushing, CI will yell at you. Fix = pre-commit hooks that force codegen + stage changes. -
cache poisoning: Sometimes a bad fingerprint or corrupted cache makes CI skip when it shouldn’t. Fix = add a manual “regen all” command (e.g.,
pnpm dc:clean && pnpm dc:gen
).
Debugging tip
If you hit weird mismatches, first nuke caches locally and in CI. 80% of “why doesn’t this match?” bugs come from stale state, not actual code.
Caching makes the whole thing snappy, but like every speed boost, it comes with the occasional banana peel. Build in an escape hatch and you’ll stay sane.
Rollout plan: adopt without breaking flow
Don’t flip everything at once. Roll it out in layers:
-
Local wrapper → introduce
pnpm dc:gen
, keep old commands for safety. - Pre-commit hook → catch missed codegen before PRs.
- CI enforcement → run wrapper in GitHub Actions, fail on missing diffs.
- Auto-publish → once trusted, let CI commit or release packages.
Even stopping at step 2 or 3 kills 90% of the pain. You can scale to auto-publishing later when the team’s ready.
Conclusion: stop babysitting codegen
Manual Firebase CLI runs are busywork. By pinning versions, hashing schemas, and hiding it all behind pnpm dc:gen
, you turn SDK generation into a zero-touch system. Locally it’s one command, in CI it’s automatic regen + tests + optional publish.
Hot take: Firebase should’ve baked this in already but until then, wrappers save our sanity. And this pattern isn’t just for Firebase; GraphQL, OpenAPI, gRPC anything with codegen benefits from the same recipe.
Got your own codegen horror stories or hacks? Share them I want to see what tricks other teams are pulling.
Helpful resources

This content originally appeared on DEV Community and was authored by