Quickstart
One command scaffolds both halves of a game into your own repository: the Cloudflare Worker that owns the rules, and the Flutter app that draws them.
Before you start
- Node.js 22 or newer, with npm or pnpm.
- Flutter 3.47 or newer, which brings the Dart 3.13 the client needs.
- Network access throughout, since scaffolding installs both halves as it goes.
- A Firebase project (free) for sign-in and push. Optional while
scaffolding, but the app throws
Firebase is not configuredat launch until one is connected. - A Cloudflare account (free) only when you deploy.
Firebase needs two CLIs, and they are the only global installs:
curl -sL https://firebase.tools | bash # the `firebase` CLI
dart pub global activate flutterfire_cli # the `flutterfire` CLI
export PATH="$PATH":"$HOME/.pub-cache/bin" # add `flutterfire` to PATH
firebase login # both CLIs share these credentials
You do not need a Firebase project yet; the scaffolder offers to create one. Prerequisites has the full toolchain, including Android.
Scaffold
pnpm create eigen-game my-game
# or: npm create eigen-game@latest my-game
my-gameis the only argument, a lowercase kebab-case slug. Everything else is asked.- The organization is the answer worth reading twice. It prefixes the
Android
applicationId, which Google Play makes permanent at first upload. - The scaffolder connects Firebase, then commits, so your first
git diffis your first game change. - Use
@latestrather than a cached copy. The scaffolder pins the engine andeigen_flutteras a tested pair.
You get one repository holding both halves:
my-game/
├── server/ # Cloudflare Worker: the authoritative rules
└── app/ # Flutter app: the screens
The details to use the scaffolder in non-interactive mode are present here.
Run the Worker
cd server
pnpm dev # applies the D1 migrations, then wrangler dev
curl http://localhost:8787/health
wrangler dev simulates D1, the Durable Objects and the cron trigger locally.
/health answering {"status":"ok"} means the whole Worker stack is up, and
rules and fixture tests need nothing more than this.
Setup the Firebase Project
1. Turn on Google sign-in. Firebase Console → Security → Authentication → Sign-in method → Get Started → Google → Enable.
2. Populate GOOGLE_WEB_CLIENT_ID If GOOGLE_WEB_CLIENT_ID in
app/app-config.json is empty, the provider was off when you scaffolded, so
there was no OAuth client to copy. Enabling it creates one; take the value from
Web SDK configuration under Firebase Console → Authentication → Sign-in
method → Google
3. Get the Web Push key. Firebase Console → Settings → General → Cloud
Messaging → Web configuration, Generate key pair if the list is empty.
Paste it into FIREBASE_VAPID_KEY in app/app-config.json. The web app will
not start without it.
4. Get the Admin service-account key. Firebase Console → Settings →
Service accounts → Generate new private key. Copy
server/.dev.vars.example to server/.dev.vars and fill
FIREBASE_CLIENT_EMAIL and FIREBASE_PRIVATE_KEY from the downloaded JSON,
pasting the private key with its quotes intact. That pair is what push
notifications and account deletion run on; token verification uses
FIREBASE_PROJECT_ID alone, so the Worker starts either way, and the two
features fail only when exercised. wrangler dev reads .dev.vars at startup,
so restart pnpm dev afterwards.
5. Link Google Analytics. If the console shows "Google Analytics not enabled for Project", that is expected: a project created through the CLI during scaffolding is not linked to an Analytics account, and only the console's own create flow offers it. Firebase Console → Settings → Integrations → Google Analytics → Enable.
Steps 2 and 3 are public values compiled into the app. Step 4 is not:
.dev.vars is a real credential, is git-ignored, and must stay that way. Delete
the downloaded JSON once you have copied the two fields out of it.
Configuration is the full reference for every value
on both sides, and what changes when they stop pointing at localhost.
If you scaffolded without Firebase
You answered yes to scaffolding without it, or passed --no-firebase, so
app/lib/firebase_options.dart is still the throwing seam, the app will not
launch, and nothing above was filled in. Run this once from the repository root:
pnpm firebase:configure
# add `-- --project my-project-id` to skip the project picker
It configures Android and web with FlutterFire, writes the service worker's
Firebase configuration, and fills in FIREBASE_PROJECT_ID and
GOOGLE_WEB_CLIENT_ID exactly as scaffolding would have. It ends by naming the
project, both app IDs, and each value it set, so you can see which of the steps
above are still owed. It does not touch steps 3 and 4, which are console values
no CLI can produce.
Commit everything it writes: app/firebase.json,
app/android/app/google-services.json, app/lib/firebase_options.dart,
app/web/firebase-config.js and FlutterFire's two Android Gradle edits. They
are public app identifiers rather than credentials, they are not git-ignored,
and Android and web builds fail without them.
Run the app
cd app
flutter run -d chrome --web-hostname localhost --web-port 7357 \
--dart-define-from-file=app-config.json
Port 7357 is the WEB_APP_ORIGIN the Worker scaffold already trusts, so leave
it as it is. Pass --dart-define-from-file=app-config.json to every
flutter run and flutter build; there is no generated config class.
The development loop
wrangler dev reloads Worker source on save, which covers most rules work. What
it does not do is regenerate anything: game-contract.json and the Dart it
produces are build outputs, not watched files.
cd server && pnpm run test:watch # keep the fixture runner going while you edit
pnpm run contract # from the root, after a schema or fixture change
cd app && flutter test # check the Dart side agrees
Bring an agent
A game is six pure functions and a widget against a sharply-specified contract, which is work a coding agent does well once it knows the contract. Install the skill that states it:
/plugin marketplace add eigeninteractive/eigen-platform
/plugin install eigen@eigeninteractive
That adds building-a-game, which loads when the work is writing or reviewing
an EigenInteractive game and carries the four invariants, what the engine has
already validated before your hook runs, and a review checklist. It ships from
the engine repository, so it moves with the engine.
Working with an agent adds the Cloudflare and Flutter skills for the other 90% of the repository, the retrieval surface that keeps an agent reading current documentation, and the mistakes worth reviewing for on this contract.
What a game is
Four Zod schemas and a handful of pure hooks, in one
GameRules unit:
stateis the authoritative truth, held by the Worker and never sent to a player.observationis the slice one seat is allowed to see, computed fromstate. It is what the app draws, and where hidden information is enforced.actionis what a player submits;configis what the creator chose before the game started.- Each unit is registered under a schema version, and shipped versions are
immutable: an incompatible change becomes a new
v2unit besidev1.
See Payload types, The hooks and Versions.
Change the rules
The seeded game is a race to a target count. Open server/src/module/v1.ts and
bound what a player may add per turn:
const actionSchema = z.object({ amount: z.int().min(1).max(3) }).meta({
id: "ExampleGameV1Action",
});
That is a schema change, so regenerate the contract from the repository root:
pnpm run contract
It rewrites server/game-contract.json (every schema version plus the shared
fixtures), and from it the Dart payload types and fixture copies in app/.
Commit both: they are the boundary between the two halves, and the app now
rejects amount: 4 before it ever reaches the Worker.
Changing hook behaviour rather than a schema works the same way, except that
you update the fixture in server/src/module/fixtures/v1/ alongside it. The
fixtures are part of the contract, and both languages run them. That is what
keeps the app's prediction and the Worker's ruling from drifting apart.
Before anything has shipped, edit the seeded v1 unit freely.
Next: Your first game walks through Rock–Paper–Scissors in both languages. Prefer to wire the two halves up yourself, or add EigenInteractive to an app you already have? See Set up without the scaffolder.