Identity
Every site in the
KATFORGE ecosystem shares one identity layer. A player who signs up on Stumper.gg is automatically a player on Lextris, Gear Goblins,
wc3stats, and any future site — same player_id, shared progression, single sign-on across domains.
erDiagram
players ||--o{ users : "registered"
players ||--o{ guests : "anonymous"
players ||--o{ stumper_stats : "game data"
players ||--o{ lextris_results : "game data"
players ||--o{ gg_characters : "game data"
players ||--o{ oauth_identities : "social login"
players {
int id PK
int user_id FK "null for guests"
int guest_id FK "null for registered"
string name
string[] roles
}The players table
public.players is the universal identity. Every game table references player_id, never user_id or guest_id directly. This means a guest can play, accumulate stats, and later upgrade to a registered account without losing anything.
Three account states
| State | How it starts | What they can do |
|---|---|---|
| Guest | POST /v1/gateway/guest | Play games, appear on leaderboards, accumulate stats. No email, no password. |
| Registered | POST /v1/users or upgrade from guest | Everything a guest can do, plus: log in from any device, link OAuth providers, manage communication preferences. |
| OAuth-only | GET /v1/gateway/oauth/{provider}/url flow | Registered via Discord/Google/Apple/Steam without ever setting a password. |
Broadly integrating identity
The SDK owns auth state and token refresh; @katforge/spark gives you a pre-built login modal and a Vue plugin that shares the same SDK instance across every component.
1. Install
npm install @katforge/api @katforge/spark
2. Instantiate and register
Create one SDK instance, then register @katforge/spark as a Vue plugin. Spark provides the SDK via Vue's provide/inject, so SparkLogin, SparkAvatar, and any component that calls useApi () share the exact same auth state. useApi () returns that registered KATforge instance; Spark doesn't own auth state — the SDK does.
import { createApp } from 'vue';
import { KATforge, browserStorage } from '@katforge/api';
import { createSpark } from '@katforge/spark';
import App from './App.vue';
const katforge = new KATforge ({
baseUrl: 'https://api.katforge.com',
storage: browserStorage
});
// Exchange the refresh cookie for an access token and hydrate katforge.player
await katforge.init ();
createApp (App)
.use (createSpark ({ katforge, theme: 'stumper' }))
.mount ('#app');
Cross-platform (Capacitor) apps
Apps that ship to iOS and Android through Capacitor run on top of @katforge/forge, the
KATFORGE engine package. Forge exports tokenStorage, a drop-in that implements the standard DOM Storage interface and selects the right backend per platform: window.localStorage on web, secure storage (iOS Keychain / Android EncryptedSharedPreferences) on native.
import { KATforge } from '@katforge/api';
import { tokenStorage } from '@katforge/forge';
const katforge = new KATforge ({
baseUrl: 'https://api.katforge.com',
storage: tokenStorage
});
The rest of the tutorial (Spark registration, init(), login UI, reactive player) is identical.
3. Drop in the login UI
For most cases, prefer SparkUserMenu over wiring SparkLogin directly. It bundles the Sign In button, the modal, the avatar dropdown, and the wallet display into one themeable component:
<script setup>
import { SparkUserMenu } from '@katforge/spark';
</script>
<template>
<SparkUserMenu
variant="ghost"
mode="hover"
:providers="[ 'discord', 'google' ]"
/>
</template>
Going with the lower-level pieces is fine when you need full control. SparkLogin is a modal that covers every entry path on the Three account states table: email/password, OAuth providers, guest sessions, passwordless codes, and forgot-password. Mount it once, anywhere in the tree.
<script setup>
import { ref } from 'vue';
import { useApi, SparkLogin } from '@katforge/spark';
const api = useApi ();
const open = ref (false);
</script>
<template>
<button v-if="! api.player" @click="open = true">Sign in</button>
<span v-else>Hi, {{ api.player.display_name }}</span>
<SparkLogin v-model="open" />
</template>
4. Track the current player reactively
@katforge/spark ships a usePlayer () composable that returns a Vue ref tracking the authenticated player. Every login, logout, token refresh, and guest upgrade updates it automatically; the listener cleans up on unmount.
<script setup>
import { usePlayer } from '@katforge/spark';
const player = usePlayer ();
</script>
<template>
<div v-if="player">Welcome back, {{ player.display_name }}</div>
<div v-else>Not signed in</div>
</template>
Under the hood it subscribes to the SDK's auth:change event. If you need a lower-level hook (e.g. writing to a Pinia store, running effects on every transition), call api.on ('auth:change', handler) directly.
5. Call the API as the signed-in player
Every SDK call attaches the access token and transparently refreshes on 401. Your code never touches the token directly.
const api = useApi ();
const state = await api.games.stumper.state ();
6. Sign out
await api.auth.logout ();
Clears tokens, fires auth:change with player: null, and every consumer re-renders.
Guest → registered upgrade
// Guest session
const session = await katforge.auth.guest ();
// ...player accumulates game data...
// Later, upgrade — player_id and all game data are preserved
await katforge.auth.upgrade ({
email: 'anders@example.com',
password: 'hunter2'
});
The upgrade converts the guest row into a full user row and links them to the same players.id. No data migration, no second account.
OAuth-only accounts
Registering via Discord, Google, Apple, or Steam creates a real users row with ROLE_REGISTERED and password = null. These accounts are first-class: they can play every game, link additional providers, manage communication preferences, and sign in across every
KATFORGE site. The only thing they lack is a password, so they can only sign in through a linked provider.
Adding a password
OAuth-only users add a password through the standard reset flow, using the email the provider returned on link. No "set initial password" endpoint exists; the reset flow handles both cases.
// User clicks "Add a password" in account settings
await katforge.auth.password.forgot (player.email);
// They open the email, follow the link, and call password.reset (token, newPassword)
// on the reset page. Same endpoints as a forgotten-password flow.
Once a password is set, the account behaves exactly like an email/password registration. The linked providers remain; they're additional sign-in methods, not an exclusive one.
Lockout protection
Unlinking the last provider is refused while the account has no password. Without a password and without a provider, the user could never sign back in. The API returns 400 Cannot unlink last login method until they either set a password or link another provider.
Cross-domain SSO
When a user is logged in on katforge.com and clicks through to stumper.gg, they don't log in again. The flow:
- Originating site mints a short-lived passport (
GET /v1/tokens/passport) - Browser redirects to
api.katforge.com/v1/gateway/customs?passport=<jwt>&redirect=<url> - Customs handler validates the passport, mints fresh tokens on the destination domain, redirects
The passport lives ~60 seconds. Long enough for a redirect, short enough that a leaked URL is useless.
OAuth providers
Discord, Google, Apple, Steam, plus
KATFORGE's own OAuth server for first-party apps. Providers link to an existing account (multiple providers per player) or create a new one. Unlinking the last provider is refused to prevent lockout.
Avatars
Avatars are part of the player record too. players.avatar_icon / avatar_color / avatar_bg / avatar_src drive a server-composed SVG the API renders at /v1/players/{id}/avatar. Guests get a deterministic icon from their player.id; registered users can claim a globally-unique game-icon or point at a linked OAuth provider's picture. See the avatars platform doc for the full system.