Skip to main content

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
└── wrangler.jsonc

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 package plus the two Firebase libraries imported by the app entry point:

flutter create --empty --platforms android,web --org com.example my_game
cd my_game
flutter pub add eigen_flutter firebase_core firebase_messaging

flutter_local_notifications, used by the engine 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 app imports the framework through its barrel only:

import 'package:eigen_flutter/eigen_flutter.dart';

Create lib/game/module.dart, register the same version keys as the TypeScript module, and call runEngineApp 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, optional APP_HOST, and the public FIREBASE_VAPID_KEY. Read them once with const String.fromEnvironment in main.dart, pass them into EngineConfig, and 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_flutter: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 EngineConfig; 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
dart run eigen_flutter: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.