Authentication
Kraty's layered auth model. SDK key authenticates the game, player secret authenticates the player. Plus the API-surface security boundary and key rotation guidance.
Kraty has three authentication surfaces, each with a different trust posture. Understanding which credential belongs where is the most important security decision you make integrating Kraty.
TL;DR
| Surface | Audience | Credential | What it can do |
|---|---|---|---|
/sdk/v1 | Game clients (mobile, desktop, web) | client_sdk API key + per-player secret | Drive a player's own attempts, claim rewards, spend wallet / inventory |
/server/v1 | Studio backend services | server_integration API key | Mint currency, grant items, issue manual grants, manage IAP fulfilment |
/admin/v1 | Portal staff (operators) | Member session (OAuth) | Configure events / catalog / API keys, audit logs, force-claim |
The cardinal rule: server_integration keys never touch a
game client. Anyone who can extract them owns every player's
economy.
Layer 1: SDK key (the game)
Every game in the portal has at least one client_sdk API key.
You bake one into the game build for each environment (test,
live).
final kraty = Kraty(KratyClientOptions(
apiKey: '<your-client-sdk-key>',
));The key is sent as Authorization: Bearer <key> on every
request. It tells the backend "this request is from game X in
studio Y, with permission set Z." It does not tell the
backend which player.
What's protected by SDK key alone
Public catalog data. Anyone with the SDK key can read it without proving they are a specific player:
GET /sdk/v1/players/:id/events(list available events)GET /sdk/v1/leaderboards/:key(configurable board snapshot)GET /sdk/v1/event-leaderboards/:id(per-event-window snapshot)GET /sdk/v1/event-leaderboards/:id/stream(SSE stream)GET /sdk/v1/lobbies/:id(lobby state)
This is intentional. A leaked SDK key should not compromise player data, only let an attacker query game configuration.
Key rotation
Keys are revocable in the portal under
Game → Settings → API Keys. Revoking takes effect immediately.
Each key carries an environment (live or test) and a
permission set (client_sdk or server_integration).
Layer 2: Player secret (the player)
Every player gets a per-player secret on first contact via
POST /sdk/v1/players/:externalId/register. The secret proves
"this request is genuinely from THIS player." Combined with the
SDK key, the server has the full identity it needs to authorise
mutations.
You almost never call players.register directly; every
official SDK lazily registers + persists the secret on the first
player-scoped call:
const kraty = new Kraty({ apiKey: '<your-client-sdk-key>' });
// First call below registers the player, mints the secret, and
// persists both. Subsequent calls reuse the persisted identity.
await kraty.events.listForPlayer();The same one-call setup ships in the Flutter and Unity SDKs. See each SDK's auth section for the persistence backend chosen on that platform and how to override it.
What's protected by SDK key + player secret
Anything that reads or mutates a specific player's state:
POST /sdk/v1/players/:id/events/:key/start(paysentryCost)POST /sdk/v1/players/:id/events/:key/attempts/:id/progressGET /sdk/v1/players/:id/inventoryPOST /sdk/v1/players/:id/inventory/:key/consumeGET /sdk/v1/players/:id/walletPOST /sdk/v1/players/:id/wallet/:key/debitGET /sdk/v1/players/:id/pending-grantsPOST /sdk/v1/players/:id/grants/:id/claimPOST /sdk/v1/players/:id/crates/:id/open
Bob's secret on Carol's route → 401 player_secret_invalid. A
leaked SDK key with no per-player secret → 401 on all of the
above.
Cross-player attack: what's blocked
attacker has: leaked SDK key (e.g. dumped from a competitor's APK)
attacker tries:
GET /players/legitimate_player/inventory → 401 player_secret_invalid
POST /players/legitimate_player/wallet/gold/debit → 401 player_secret_invalid
POST /players/legitimate_player/events/x/start → 401 player_secret_invalidThe attacker can still:
- Read the events catalog (
events.listForPlayeris not gated). - Read leaderboards.
- Register a NEW player and play normally as themselves.
Which is the right blast radius: the player gate prevents anyone from spending or reading another player's specific state.
Storage
Two-layer auth is only as strong as the device storage. Each SDK picks a sensible default for its platform out of the box:
| Platform | Default backend |
|---|---|
| Browser / React Native (TS SDK) | LocalStorageSecretStore (wraps window.localStorage) |
| Node / SSR / workers (TS SDK) | InMemorySecretStore (volatile; pass a custom store for durability) |
| Flutter mobile / desktop / web | SharedPreferencesSecretStore (wraps shared_preferences) |
| Unity | PlayerPrefsSecretStore (wraps UnityEngine.PlayerPrefs) |
For higher-value economies, plumb a custom SecretStore (browser:
sessionStorage or IndexedDB; Flutter: flutter_secure_storage;
Unity: a platform-specific keychain wrapper). Every SDK exposes
the interface and lets you pass an instance through the options
object. See the per-platform docs.
Recovery when a secret is lost
If a player wipes app data or switches devices, the local secret
is gone but the server still has the hash. Calling register
again returns 409 player_already_registered. You have two
paths:
-
Production: surface "account recovery" UX, letting the player log into a linked identity (Apple / Google / email), then have your studio backend call
/admin/v1/players/.../rotate-secretto issue a fresh secret which you hand back to the client. -
Dev / test environments: call
register?force=trueand the backend rotates the secret in-place. Only accepted on non-liveAPI keys so a production-key leak cannot be used to lock out every player.
The official SDKs surface the 409 through
KratyApiError.isPlayerAlreadyRegistered. For a production
recovery flow, catch the error and run path (1). In dev / test
environments only, you can re-issue the secret by hitting
POST /sdk/v1/players/:id/register?force=true from your own code;
live API keys reject force.
Layer 3: Server integration (your backend)
/server/v1 is the studio backend's call surface for fulfilment
operations that must NEVER reach the player's device:
POST /server/v1/players/:id/wallet/:key/credit(IAP fulfilment)POST /server/v1/players/:id/wallet/:key/debit(refunds)POST /server/v1/players/:id/inventory/:key/grant(entitlement)POST /server/v1/players/:id/inventory/:key/revoke(chargebacks)POST /server/v1/players/:id/grants(manual server-issued grants)
These bypass the player secret (your backend is trusted) but
require a server_integration API key and idempotency keys.
Never put a server_integration key in a game client. Not
in build configs, not in obfuscated strings, not on a build
flag that "only loads in dev." If it can reach the player's
process, it can be extracted. Use
@kraty/server-sdk (Node) or
kraty-server-sdk (Python) from a
backend you control. The Flutter and Unity SDKs deliberately
do not expose /server/v1 endpoints to make this hard to get
wrong.
Correct production architecture:
┌─────────────┐ Authorization: Bearer <client_sdk key>
│ Game client │────── X-Player-Secret: <per-player> ──→ ┌────────┐
└─────────────┘ │ │
│ Kraty │
┌─────────────┐ │ │
│ Studio │─────── Authorization: Bearer ──────────→ │ │
│ backend │ <server_integration key> └────────┘
└─────────────┘
▲
│ HTTPS (your auth)
│
┌─────────────┐
│ Game client │
└─────────────┘Game client talks to Kraty for player flows. Studio backend talks
to Kraty for fulfilment / admin flows. Game client also talks to
studio backend for things like IAP receipt verification: the
studio backend validates the receipt with Apple / Google, then
calls /server/v1/.../wallet/credit on Kraty.
Layer 4: Portal members (operators)
The portal authenticates studio members via OAuth (Google today)
and issues HTTP-only session cookies. All /admin/v1 calls
require a member session + permission grant.
This surface is never consumed by SDKs; it is the portal UI talking to its own backend. Operators use the portal to configure events, manage keys, run audits, force-claim stuck grants, etc.
Audit logging
Every credentialed write surfaces in audit logs:
client_sdkcalls →core.api_keys.last_used_at+ log of the operationserver_integrationcalls → audit log with the API key prefix- Portal member actions → audit log with the actor's member id
- Player register / rotate → audit row with the originating IP
Portal users can read the audit log under Studio → Audit Log scoped per game.
Production checklist
Before shipping a Kraty integration to production, verify:
- Only
client_sdkkeys in game build outputs - Game build uses an
environment: 'live'key (test keys rejectforce=truerotations; see Recovery) - Player secret stored via platform secure storage (not just
plain
SharedPreferencesfor high-value economies) - All IAP fulfilment goes via your backend with
server_integrationkey, not direct from the client - Account recovery flow defined for lost secrets (your backend can call admin rotate)
- Audit log alerts wired up for unusual
server_integrationkey activity