Follow this guide when you want players to accumulate something between matches: cards, currency, unlocks, stats. Start with the concept doc on persistent profiles if you haven’t seen the model yet. Two working demos live in the repo:Documentation Index
Fetch the complete documentation index at: https://openturn.io/docs/llms.txt
Use this file to discover all available pages before exploring further.
examples/using-profiles/persistent-wins— gamekit flavor. Recommended starting point.examples/using-core/persistent-wins-core— same game in raw@openturn/core. Useful if you’re authoring without gamekit.
+1 wins. Everything below uses the gamekit version; swap @openturn/gamekit for @openturn/core if you’re on raw core.
1. Declare the profile
defineProfile is re-exported from @openturn/gamekit for convenience; it’s the same function as @openturn/core’s.
Rules of thumb:
defaultis the first-play shape. Players who never had a profile get this on their first match.parseruns on every hydration. It’s your only defense against stored data from an older schema. Write it defensively.commitis pure. No timers, no randomness, no external reads. Inputs are the match, a bound profile mutation helper, the profiles as they were at setup, and the terminal result. Use direct helpers likeprofile.inc(...)for simple writes andprofile.update(...)for complex draft mutations.
2. Attach it to your game
move.finish({ winner }) fires, gamekit sets result.winner and the engine calls profile.commit. The host persists the returned delta.
3. Test locally
The engine runs identically in local and cloud modes. UsecreateLocalSession to exercise the full lifecycle in a unit test:
- You supply
match.profilesat session creation. Missing entries are auto-filled fromprofile.default. - The commit is a pure function call on the host side — you run it whenever the session’s
getResult()is non-null. applyProfileCommithydrates missing defaults, computes the delta, applies it, and reports any rejected per-player ops.
persistent-wins/game/src/index.test.ts.
4. Persist locally (single-player app)
For a local app, wrap the engine with whatever storage you prefer. A minimal pattern:applyCommitLocally(profiles, result) helper (source) — copy that shape if you want.
5. Deploy to openturn-cloud
When you deploy withopenturn deploy, the cloud takes over hydration and commit. You don’t configure anything — it works automatically because:
- Hydration: when a match activates, the game-worker Durable Object calls back to
/api/profiles/hydratewith the seated user IDs. The cloud reads theprofiletable and returns each user’s stored data (or nothing if they’re new — the engine fills inprofile.default). - Commit: when the match terminates, the DO runs your
profile.commitand POSTs the delta to/api/profiles/commit. The cloud re-validates against the current profile and writes it. - Auth: both calls are signed with HMAC-SHA256 using the room-token secret that’s already shared between cloud and every game-worker. No new env vars, no per-deploy configuration.
profile / profile_commit tables exist. They’re part of the normal Drizzle migrations under openturn-cloud/drizzle/. Run your usual migration command.
Check progress from the client:
{ profile: null } for first-time players).
Security caveats
Profiles are scoped bygameKey, not by project. That’s what lets you promote a new deployment without resetting every player’s collection — but it also means any deployed project that claims the same gameKey can read and write those profiles. For v1 this is flat-trust: whoever deploys first wins.
Before external authors can publish into your cloud, wire a gameKey ownership model (claim on first deploy, explicit grant for additional projects). The plan file tracks this as a pre-GA follow-up.
What to read next
- Concepts: Persistent profiles — the model and the trust story.
- Reference:
@openturn/core— exact API surface fordefineProfileand the delta grammar. - Reference:
@openturn/server—RoomSettleHandlerandonSettle, if you’re hosting your own engine.