Versions and compatibility
Several independently versioned packages ship from one platform repository. This page says which compatibility axes pair and what each version number is actually promising.
Every package is still on the 0.x line and the API is moving. Read the
breaking axis section before writing
a version constraint; it is not where you expect.
What pairs with what
| Docs | Engine (@eigeninteractive/*) | Wire client (eigen_api) | Flutter integration (eigen_flutter) |
|---|---|---|---|
| 0.10.x (this version) | ^0.10.0 | ^0.10.0 | 0.14.0, 0.13.0 |
| 0.9.x | ^0.9.0 | ^0.9.0 | 0.12.0 |
| 0.8.x | ^0.8.0 | ^0.8.0 | 0.11.0 |
| 0.7.x | ^0.7.0 | ^0.7.0 | 0.10.0 |
| 0.6.x | ^0.6.0 | ^0.6.0 | 0.9.0, 0.8.0 |
| 0.5.x | ^0.5.0 | ^0.5.0 | 0.7.0 |
| 0.4.x | ^0.4.0 | ^0.4.0 | 0.6.0 |
| 0.3.x | ^0.3.0 | ^0.3.0 | 0.5.0, 0.4.1, 0.4.0 |
| 0.2.x | ^0.2.0 | ^0.2.0 | 0.3.7, 0.3.6, 0.3.5, 0.3.4, 0.3.3, 0.3.2, 0.3.1, 0.3.0, 0.2.0 |
| 0.1.x | ^0.1.0 | ^0.1.0 | 0.1.0 |
The first three columns are one number. The Flutter column is not, and is listed
as exact versions rather than a range for that reason: eigen_flutter declares
which engine it speaks through its own eigen_api constraint, so the releases
pairing with a given line need not be contiguous. Retracted releases are
omitted, since they stay installable for anyone already locked to one, but the
solver will not choose them for a new project.
eigen_api is not versioned independently. It is generated from the engine's
OpenAPI spec and stamped with the engine's release version, which is what the
spec carries as info.version, so 0.2.4 there is 0.2.4 here, and they
cannot drift.
The engine bumps that version for any breaking change it ships, including ones
with no wire consequence: a change to what @eigeninteractive/rules exports
moves it just as a changed response body does. So a new eigen_api is not by
itself evidence that the wire moved, and upgrading it may cost you nothing.
Constrain on the version anyway: it is the number your package managers can
actually enforce.
eigen_flutter moves on its own clock. Its version describes its Dart API (the
widgets, providers, and the GameModule contract) and it records which engines
it works against through its own eigen_api constraint. So eigen_flutter 0.4.0
depending on eigen_api: ^0.2.0 means "this Flutter integration speaks the
engine's 0.2.x wire". There is no lockstep release, and there deliberately isn't one: the engine
breaks in ways that have no Dart-side consequence at all, and forcing a Flutter
release for each would make its version number meaningless.
eigen_shell and eigen_firebase are application-layer packages above
eigen_flutter. Each declares its own eigen_flutter constraint and moves on
its own clock; neither is another wire-compatibility axis.
What the scaffolder picks
A new project does not choose from the table above. create-eigen-game has
already chosen, and the version of the scaffolder you run decides both
halves. It writes one engine range into server/package.json and one
tested set of eigen_flutter, eigen_shell, eigen_firebase and
eigen_codegen ranges into app/pubspec.yaml, and it resolves nothing at run
time. Use @latest rather
than a cached copy:
pnpm create eigen-game my-game
npm create eigen-game@latest my-game
The two numbers reach it differently, and the difference is the point.
The engine range is derived, not maintained. The scaffolder emits a caret on
the @eigeninteractive/server version it was built against, the same engine its
CI compiled the Worker template with. Nobody types that number, so the templates
cannot ship paired with an engine no build ever saw.
The eigen_flutter range is a pin, deliberately. It once resolved "the newest
client for this engine's wire line" from pub.dev, which was wrong: a client
declares which wire it speaks, and says nothing about whether its Dart API
still matches the templates. eigen_flutter 0.4.0 constraining eigen_api: ^0.2.0 is a legal match that would emit code against an API that moved.
Stated does not mean hand-maintained. The pin is raised by the Dart release
itself, in the same commit that creates the version it names — the one commit
that knows that number without being told — and CI then scaffolds a project and
runs flutter analyze against exactly that shell before the release lands. So
the scaffolder never names the previous line, and a pin that does not compile
stops its own release rather than a later one.
So a scaffolder release trails an engine release, and that gap is real rather
than an oversight. When the engine crosses a line, no shell can speak it yet:
eigen_flutter records compatibility through its own eigen_api constraint, and
eigen_api for the new line does not exist until the engine's release publishes
it. The scaffolder keeps emitting the previous line, a pairing that works,
until a Flutter integration for the new one ships and the pin is raised. The
shell, Firebase and codegen pins move on the same mechanism, each raised by the
release that publishes it.
If you need a combination the current scaffolder does not emit, take the manual path: Set up without the scaffolder uses the same public contracts, and the table above is what to write into it.
The breaking axis is the minor, for now
Semver treats 0.x specially, and both halves of this project are in 0.x:
^0.1.0 resolves to >=0.1.0 <0.2.0
^1.0.0 resolves to >=1.0.0 <2.0.0
So while a package is pre-1.0, breakage is announced in the MINOR position
and the major position is unused. 0.1.4 → 0.1.5 is additive; 0.1.4 →
0.2.0 is the break. Once a package reaches 1.0.0 this shifts to the usual
major/minor split.
This is worth stating plainly because release tooling generally does not
translate it for you: asking for a "major" bump on a 0.1.0 package ships
1.0.0 and declares a stability guarantee by accident. While a package you
publish is pre-1.0, choose minor for breaking and patch for everything else.
Three different version numbers
The word "version" means three unrelated things in this system. Keeping them apart is most of what compatibility reasoning is:
| What it versions | Who moves it | |
|---|---|---|
| Package semver | the developer-facing API of one package | whoever publishes that package |
| The engine's breaking axis | the HTTP + socket wire contract | the engine; eigen_api mirrors it |
Game schemaVersion | one game's own state/action payloads | the game author |
The third is the one people expect to find here and won't. A game's
schemaVersion is internal to that game: the engine resolves each request
against the version the game was created at, and old games keep running under
old rules forever. It is not tied to the engine's version, and bumping one never
implies bumping the other. See Versions.
The docs are versioned on the engine's release line, because that is what
decides whether a page is still true. A page describes a task end to end, the
TypeScript rules and the Dart client together, so either half going breaking
invalidates it, and the engine's release line is the one number that moves for
both. That is why it is the release line and not the wire specifically: 0.2.0
left the wire untouched and still rewrote what a GameModule imports.
What a breaking bump means
For the engine, it means the wire changed in a way an existing client cannot absorb. Two categories are less obvious than they look:
Widening a response enum is additive. Every generated Dart enum has an
unknownDefaultOpenApi member. When an installed client meets a value introduced
by a newer server, decoding succeeds and the app can show generic or
update-required UI rather than losing the whole response.
That sentinel is deliberately read-side only. Serialising it produces
unknown_default_open_api, which no route accepts. Adding a value that clients
may optionally send is additive; changing a request so an old client must send
the new value is breaking. Removing or renaming an enum member is also breaking.
Adding a field is not breaking. The generated models are built with
disallowUnrecognizedKeys: false, so an older client silently ignores keys it
does not know. Removing a field, renaming one, or changing its type is breaking.
Why this matters more here than in a normal library
A library consumer upgrades when they choose to. An installed app does not. Once a release is in the stores, those binaries keep talking to your server for as long as people leave them installed, so an old client meeting a new server is the normal case, not the edge case, and it is a case you cannot fix by shipping a patch.
That asymmetry is why the generated client tolerates both kinds of response
widening: unknown fields are ignored, and unknown enum values become
unknownDefaultOpenApi. The server can add either without making a current app
fail response decoding.
The sentinel adds a member to every generated enum, so exhaustive switches must handle it. Enum additions reuse that same member and do not change the Dart surface.
Anything genuinely breaking needs a deprecation window: ship the additive half first, let installs turn over, and only then remove the old half.
eigen_shell checks for updates at cold start and on resume. Routine Android
checks use Play's native update flow without interrupting an active game. When
an unknown value makes one surface unsafe, that surface instead shows an
explicit update action: Play in-app update on Android, or a reload of the
current application in a browser. No store or download URL is configured by
the client framework.
For a new value that old clients cannot safely present, release in client-first order:
- Publish the compatible Android build and deploy the compatible web client while the server still emits the old vocabulary.
- Wait until the Play build is available to the full intended audience.
- Only then enable the server behavior that emits the new value.
Do not enable that behavior globally during a staged Play rollout unless the rollout already covers everyone who may receive it. The sentinel prevents a decode crash; it cannot make an unpublished or ineligible update available.
Reading the docs at the right version
The version selector in the navbar names the engine line these pages describe.
Only 0.2.x is served, and it is served at the root, so every /docs/* URL is
a 0.2.x URL.
There is no /docs/0.1.x/. This site's first public deploy already described
0.2.x, so no 0.1.x page was ever published and there was nothing to freeze.
Read the 0.1.x reference from the packages themselves: npm and pub.dev keep
every published version, and eigen_api's dartdoc is versioned alongside it.
From here on a breaking engine release freezes this line: 0.2.x moves to
/docs/0.2.x/*, those links keep working, and the root becomes the new line.