Docs/Identity

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

StateHow it startsWhat they can do
GuestPOST /v1/gateway/guestPlay games, appear on leaderboards, accumulate stats. No email, no password.
RegisteredPOST /v1/users or upgrade from guestEverything a guest can do, plus: log in from any device, link OAuth providers, manage communication preferences.
OAuth-onlyGET /v1/gateway/oauth/{provider}/url flowRegistered 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

shell
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.

TypeScript
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.

TypeScript
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:

Vue
<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.

Vue
<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.

Vue
<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.

TypeScript
const api = useApi ();
const state = await api.games.stumper.state ();

6. Sign out

TypeScript
await api.auth.logout ();

Clears tokens, fires auth:change with player: null, and every consumer re-renders.

Guest → registered upgrade

TypeScript
// 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.

TypeScript
// 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:

  1. Originating site mints a short-lived passport (GET /v1/tokens/passport)
  2. Browser redirects to api.katforge.com/v1/gateway/customs?passport=<jwt>&redirect=<url>
  3. 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.