Set up without the scaffolder
create-eigen-game is a convenience, not a framework requirement. A game is
valid when its Worker and app satisfy the two public package contracts and
share one generated game-contract.json; the directories may live together or
in separate repositories.
Use this path when you are adding EigenInteractive to an existing app, need independent Worker and app release cycles, or want to own the project layout yourself.
Create the Worker
Start a TypeScript Cloudflare Worker and add the runtime, rules contract, schema library, and test tooling:
mkdir server && cd server
npm init -y
npm install @eigeninteractive/server @eigeninteractive/rules zod
npm install --save-dev @eigeninteractive/testkit wrangler typescript vitest @types/node
pnpm works equally well. Keep @eigeninteractive/rules as a direct dependency:
server and testkit consume it as a peer so the process has one rules
contract instance.
Use this minimum application-owned layout:
server/
├── src/
│ ├── index.ts
│ └── module/
│ ├── index.ts # default export: GameModule
│ ├── v1.ts # one GameRules unit
│ └── fixtures/v1/*.json
├── test/twin.spec.ts
├── package.json
├── tsconfig.json
├── vitest.config.mts
└── wrangler.jsonc
vitest.config.mts exists for one option. The fixture runner reads its JSON with
readFileSync, so those files are not in Vite's module graph, and test:watch
would otherwise ignore a fixture-only edit:
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
forceRerunTriggers: [...configDefaults.forceRerunTriggers, "**/src/module/fixtures/**/*.json"],
},
});
Spread the defaults rather than replacing them; that merge is shallow.
Default-export the module from src/module/index.ts:
import type { GameModule } from "@eigeninteractive/rules";
import { rulesV1 } from "./v1.js";
export default { versions: { 1: rulesV1 } } satisfies GameModule;
The Worker entry point only composes your module with engine-owned runtime:
import { BaseGameDO, createEngine } from "@eigeninteractive/server";
import gameModule from "./module/index.js";
export class GameDO extends BaseGameDO<Env> {
protected readonly gameModule = gameModule;
protected d1(env: Env) => env.GAME_DB;
}
export default createEngine({
gameModule,
appName: "My Game",
d1: (env: Env) => env.GAME_DB,
gameDO: (env: Env) => env.GAME_DO,
});
Add the game name and contract commands to package.json:
{
"type": "module",
"eigen": { "game": "My Game" },
"scripts": {
"contract": "eigen-contract",
"contract:check": "eigen-contract --check",
"dev": "wrangler dev",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "wrangler types && tsc --noEmit"
}
}
The eigen.game value is the source of generated Dart type names. See
The contract for the rules unit and
Deploy the Worker for the required D1,
Durable Object, cron, and migration configuration.
Create the Flutter app
Create a normal Flutter app, then add the presentation package, optional Firebase adapter, and Firebase Core used by the generated options file:
flutter create --empty --platforms android,web --org com.example my_game
cd my_game
flutter pub add eigen_flutter eigen_shell eigen_firebase firebase_core
flutter_local_notifications, used by the Firebase adapter for foreground delivery,
requires core-library desugaring in the Android application module. The
scaffolder configures this automatically; for a hand-created app, append the
following to android/app/build.gradle.kts:
android {
compileOptions {
isCoreLibraryDesugaringEnabled = true
}
}
dependencies {
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
}
The standard Firebase app imports the presentation and adapter barrels:
import 'package:eigen_flutter/eigen_flutter.dart';
import 'package:eigen_shell/eigen_shell.dart';
import 'package:eigen_firebase/eigen_firebase.dart';
Create lib/game/module.dart, register the same version keys as the TypeScript
module, and call runEigenShell with an initializeEigenFirebase initializer
from lib/main.dart. The
Creation UI and
Rendering pages contain the two handwritten
Dart pieces.
Create app-config.json beside pubspec.yaml with API_BASE_URL,
GOOGLE_WEB_CLIENT_ID, the optional APP_HOST and AUTH_DOMAIN, and the
public FIREBASE_VAPID_KEY. Read them once with const String.fromEnvironment in
main.dart. Pass API_BASE_URL and APP_HOST into EngineConfig; pass the
three Firebase values into FirebaseAdapterConfig. Use
--dart-define-from-file=app-config.json for both Android and web commands.
The complete shape and validation rules are in
Configuration.
After installing and authenticating the Firebase and FlutterFire CLIs, run the engine's setup executable from the Flutter repository root:
dart run eigen_firebase:configure_firebase
It generates FlutterFire's platform files and
web/firebase-config.js for the messaging worker from the same selected Web
app. Keep the scaffold's firebase-messaging-sw.js and
flutter_bootstrap.js; do not duplicate Firebase identifiers by hand.
For web, add the Firebase Messaging service worker and register it from a custom
web/flutter_bootstrap.js; configure a fixed local origin in the Worker and
Firebase. The scaffold supplies those files automatically. Manual projects can
copy the small setup from Deploy the web app.
Pass the project's public VAPID key into FirebaseAdapterConfig; the web app
treats a missing key as deployment misconfiguration rather than disabling
notifications.
On the Worker, set FIREBASE_PROJECT_ID and store that project's
FIREBASE_CLIENT_EMAIL and FIREBASE_PRIVATE_KEY as secrets. Those Admin
credentials are required for both FCM and complete account deletion; player
permission and individual push delivery remain optional at runtime.
Connect the halves
Emit the contract from the Worker:
cd server
npm run contract
Transfer that exact game-contract.json to the app repository, then generate
the Dart payloads and copied fixtures:
cd app
flutter pub add --dev eigen_codegen
dart run eigen_codegen:generate_payloads \
--contract path/to/game-contract.json \
--output lib/game/generated/payloads.dart \
--fixtures-output test/fixtures
flutter test
Commit both generated outputs. In CI, run npm run contract:check in the
Worker and the Dart generator with --check in the app.
For separate repositories, promote the contract as an immutable build artifact and pin it by checksum. The app does not need Worker source, and the Worker does not need Flutter source. See The cross-repository contract.