Skip to main content

The scaffolder

create-eigen-game writes one repository holding both halves of a game, wires them together, connects Firebase, and commits the result. It is a convenience rather than part of the engine: nothing it produces is private, and Set up without the scaffolder reaches the same place by hand.

Quickstart is the short version. This page is the whole of the command, and the reasoning behind the parts of it that look strict.

One argument, and everything derived from it

pnpm create eigen-game my-game
# or: npm create eigen-game@latest my-game

my-game is the only naming argument, a lowercase kebab-case slug. From it the scaffolder derives:

DerivedExampleUsed as
Display nameMy GameThe app title and the commit message
Dart packagemy_gameThe Flutter package, and the suffix of the applicationId
Type prefixMyGameThe generated payload types in both languages

Everything else is a question.

Every question has exactly one flag

Pass the flag and the question is skipped. Leave it out and you are asked. That one-to-one mapping is deliberate: the flag list and the question list are the same list, so there is nothing a prompt can set that a script cannot.

QuestionFlagDefault
The organization, which prefixes the permanent applicationId--org <reverse-domain>com.example
Whether to scaffold without Firebase (asked only when the CLIs are missing)--no-firebaseno, stop and set them up
Which Firebase project to configure against--firebase-project <id>FlutterFire asks
Initialise a repository and commit the scaffold--git, --no-gityes
Emit the GitHub Actions workflows--workflows, --no-workflowsno
Which package manager the generated scripts use--package-manager npm|pnpmwhichever invoked it

--help prints the same list, and -v prints the version, which is also the version pairing (see below).

The organization is the one permanent answer

The Android applicationId is the organization plus the game name, and Google Play makes it permanent at the first upload. The prompt shows that:

│ Prefixes the Android applicationId, which Google Play makes permanent at first upload.
│ Also the Android app registered in the Firebase project you pick next.

◆ Organization in reverse domain notation
│ dev.yourname.games.my_game

The .my_game is dimmed and grows as you type, because the organization is the prefix and the game name is appended to it, exactly as flutter create --org does. Answering dev.yourname.games.my_game, which reads like the whole identifier, would produce dev.yourname.games.my_game.my_game, so the CLI spots the doubled suffix and offers to shorten it.

The Firebase question that follows is the second half of the same decision: the applicationId above is what gets registered in the project you pick.

Firebase is asked first, and can end the run

Before anything is written, the scaffolder checks three things: the firebase CLI, the flutterfire CLI, and whether you are signed in. When any is missing it lists all of them at once, with the command that fixes each, and asks whether to scaffold anyway.

The default is no. Almost everything in a scaffolded project runs without Firebase (the rules, the fixtures, the Worker, its tests), but the app throws Firebase is not configured the moment it launches, because identity is how a player gets a seat at all. Stopping costs two commands; carrying on costs a project that cannot be run.

Nothing is written when it stops, and the exit code is non-zero only so a script that wrapped the command can tell there is no project.

This question is asked before the organization for the same reason: it is the only answer that can end the run, and ending it afterwards would mean having asked for a permanent decision about a project that never gets created.

Answer yes, or pass --no-firebase, and firebase:configure picks the step up later.

What it writes

my-game/
├── package.json # one contract / contract:check command
├── server/ # Cloudflare Worker: the authoritative rules
└── app/ # Flutter app: the screens

Both halves are installed as it goes, npm for the Worker and pub.dev for the app, so the run needs network access throughout rather than only at the start. An interrupted run leaves a partly installed project on disk; delete the directory and start again.

Then it commits, so your first git diff is your first game change rather than the ninety generated files underneath it. Firebase is configured before that commit, which is why it happens during the scaffold rather than as your first diff: six files, four of them edits to files the scaffold had just written.

The git question is not asked at all when the destination is already inside a repository, since the scaffolder declines to nest one there whatever it is told. It says so in the closing summary.

See Project layout for what each directory owns and which generated files cross between them.

What it fills in, and what it cannot

A deployment has values that no template can contain, because they do not exist until a Firebase project does. The scaffolder copies the ones it can, from what FlutterFire wrote moments earlier:

ValueWhere it landsRead from
FIREBASE_PROJECT_IDserver/wrangler.jsoncThe project FlutterFire recorded in app/firebase.json
GOOGLE_WEB_CLIENT_IDapp/app-config.jsonThe "client_type": 3 entry of the google-services.json it downloaded
API_BASE_URLapp/app-config.jsonNothing. It is http://localhost:8787, which is where pnpm dev puts the Worker

The wrangler.jsonc edit rewrites that one assignment in place rather than decoding and re-encoding the file, which would delete every comment in it. Cloudflare recommends JSONC for new projects, and it is a file its owner edits, so the comments are worth keeping. It insists the key appear exactly once and writes nothing otherwise, which is what makes rewriting in place safe.

Two values are left, and the closing summary names exactly the ones that apply:

  • FIREBASE_VAPID_KEY, always. A Web Push certificate is not something the Firebase CLI serves, and the web target refuses to start without one.
  • GOOGLE_WEB_CLIENT_ID, when Firebase had not created that OAuth client yet. It appears when the Google sign-in provider is enabled, which is a console action no CLI performs, so a project that never had it enabled has an empty oauth_client array and there is nothing to copy.

None of this is the scaffolder's own code. configure_firebase does the writing, and the scaffolder invokes it as --worker ../server, which is the flag that widens it from the app to the Worker beside it. The generated firebase:configure script passes the same flag, so running that command later lands in exactly the same place a scaffold does. An app-only repository omits the flag and gets the app half.

It happens between the Firebase step and the commit, so the filled-in values are part of the scaffold commit rather than the project's first diff.

Where each value comes from is the full list, including what to change at deploy.

Workflows are opt out, and can be added later

release.yml needs an upload keystore and a Play service account, and fails on every push until both exist. That is noise for a project on its first day, so the workflows default to off:

npx create-eigen-game add workflows

Run it in an existing project when shipping is the next step. See Release to the Play Store.

With no terminal, an unanswered question is an error

CI, a pipe, an agent session: there is nowhere to ask, so every answer has to arrive as a flag, and a missing one stops the run rather than being chosen for you.

--org is why. A non-interactive run that quietly defaulted it would ship com.example.my_game, and Google Play makes that permanent at the first upload. A default applied where nobody can see it is exactly how that happens.

Every unanswered question is collected and reported together, and the message prints the whole command to re-run with each default already filled in, so the fix is one paste and the value worth changing is visible in it:

npx create-eigen-game my-game --no-firebase --org com.example --git --no-workflows

Versions are pinned as a tested pair

The engine and eigen_flutter versions live inside the scaffolder and are released together, so the create-eigen-game you run decides both. A new project starts on a pairing already known to work rather than on whatever was newest that morning, which is why @latest matters more than it usually does: a cached older copy pins an older pair.

See Versions and compatibility for the table, and for what to do when you want a combination the current scaffolder does not emit.

What it deliberately is not

There is no server-only or app-only mode, and no private runtime contract that only a scaffolded project satisfies. A game is valid when its Worker and app satisfy the two public package contracts and share one generated game-contract.json. That is the whole requirement, and Set up without the scaffolder is the same result assembled by hand, with the added property of needing no network access at project-creation time.