Skip to content

krew.io on Yumina: integration notes for the krew team

This is the practical companion to the design spec (docs/superpowers/specs/2026-09-12-krew-yumina-identity-handoff-design.md). It lists exactly what Yumina exposes and what krew has to do with it.

What Yumina gives you

ThingWhere
The page players land onhttps://yumina.io/krew (optionally ?path=<encoded krew path+query>)
Your game, framedhttps://play.krew.io/<path>?host=yumina inside that page
Public configGET https://yumina.io/api/partners/krew/config
Public signing keys (JWKS)GET https://yumina.io/api/auth/jwks
Identity token (page fetches it, you never call this)POST https://yumina.io/api/partners/krew/token

Dev: replace https://yumina.io with the Yumina dev API origin we give you, and https://play.krew.io with whatever origin your dev client runs on. Yumina's dev config points at your origin through its KREW_CLIENT_ORIGIN variable.

Yumina is not in the game's network path

The frame at play.krew.io keeps talking to game.krew.io directly, HTTP and WebSocket, exactly as today. Yumina never proxies or redirects that traffic and never sees it. The whole integration is three calls:

  1. The Yumina page fetches an identity token from Yumina's own API.
  2. The page hands the token to your frame with postMessage; your frame POSTs it to your own game.krew.io/auth/yumina/session and gets a krew session back.
  3. Your server fetches Yumina's public keys from the JWKS URL once and caches them, the way it does with Google's keys.

So game.krew.io needs to allow the play.krew.io origin (it most likely does already), and nothing for yumina.io. Heads-up: your server's env validation requires the API host to equal the site host or be a subdomain of it, for the cookie domain. play.krew.io + game.krew.io fails that check; a site URL of https://krew.io passes and its cookie domain covers both subdomains. With bearer tokens the cookie hardly matters, so relaxing the check is fine too.

The redirect on krew.io

Every krew.io URL except the CrazyGames embed and play.krew.io should 301 to:

https://yumina.io/krew?path=<URL-encoded original path + query>

Example: https://krew.io/clan/abc?tab=membershttps://yumina.io/krew?path=%2Fclan%2Fabc%3Ftab%3Dmembers.

Yumina validates path (must start with /, not //, max 512 chars, no control characters or backslashes) and frames https://play.krew.io/clan/abc?tab=members&host=yumina. Anything invalid falls back to /.

Detecting that you are inside Yumina

The frame URL always carries host=yumina. Use it to select the Yumina platform provider in the client SDK (alongside crazygames, adinplay, local). A referrer check on yumina.io is a fine fallback.

The postMessage bridge

Your frame talks to the Yumina page with window.parent.postMessage(msg, "https://yumina.io"). Always pass the explicit target origin. Only accept messages where event.origin === "https://yumina.io" (or the dev origin).

Frame → page:

json
{ "type": "yumina:auth:request" }
{ "type": "yumina:auth:login" }
{ "type": "yumina:game:ready" }
  • yumina:auth:request: send on boot and whenever you need a fresh identity (your session expired, reconnect). The page answers with one of the two auth messages below. If the Yumina session is still loading, the answer waits for it rather than saying "nobody".
  • yumina:auth:login: send when the player presses your "Sign in with Yumina" button. The page navigates the top window to Yumina's login and returns to the same /krew?path=... afterwards, then your frame boots again and requests.
  • yumina:game:ready: optional, hides Yumina's loading overlay early (it also hides on the iframe load event).

Page → frame:

json
{ "type": "yumina:auth:token", "token": "<jwt>", "expiresAt": "2026-09-12T10:00:00.000Z" }
{ "type": "yumina:auth:unauthenticated" }
{ "type": "yumina:auth:changed" }
  • yumina:auth:token: exchange it immediately (see below). Do not store it.
  • yumina:auth:unauthenticated: nobody is signed in. Guests may play; show your sign-in button whenever you want.
  • yumina:auth:changed: the Yumina session flipped in another tab (login or logout). Send a new yumina:auth:request.

The identity token

A JWT, alg: RS256, kid in the header. Verify against the JWKS URL above with key caching (same as your Google id_token code). Reject if iss, aud, exp or the signature fail.

ClaimValue
isshttps://yumina.io
audkrew.io
subYumina user id. Your permanent key for this player.
iat, expLifetime is 90 seconds.
jtiRandom id, in case you want replay protection.
emailAccount email
email_verifiedboolean
usernameYumina handle, unique, may be null on very old accounts
display_nameWhat Yumina shows for the player. Never empty.
pictureAbsolute https:// avatar URL, or null
identities{ "google": "...", "discord": "...", "twitter": "..." }, only present providers

identities.google is Google's OpenID sub and identities.discord is the Discord user id, the same values your auth.googleId / auth.discordId already hold. Google sub, Discord ids and X ids are global per user, not per OAuth app, which is why they match across the two products.

Suggested krew-side flow (your call)

  1. POST /auth/yumina/session { token } on your API, a sibling of the CrazyGames session route: verify → find or create → signSessionJwt → return { token } for the bearer header.
  2. Add auth.yuminaId to the user model, indexed like the other providers.
  3. Match order: auth.yuminaId == sub wins. Otherwise look up identities.google / identities.discord against legacy accounts that have no yuminaId yet. 0 → new account. 1 → attach yuminaId. 2 → merge, log, snapshot, attach.
  4. Sync display_name and picture on each session start, the way you sync OAuth avatars today.
  5. Inside the frame, accept postMessages only from https://yumina.io (and the dev origin). That is the only place the Yumina origin appears on your side.

Testing together

  • Yumina dev exposes the same routes; we hand you the dev origin and set KREW_CLIENT_ORIGIN to your dev client.
  • Use a Yumina account with both Google and Discord linked (Settings → Connected accounts) against a krew dev database seeded with one Google-made and one Discord-made legacy account to exercise the 0 / 1 / 2 match cases.
  • GET /api/partners/krew/config returning enabled: false means Yumina has the feature switched off; the page shows "not available yet".