Step 1 — Register your game in the database

Before the SDK can create sessions for your game, it must be registered in the Play Hub shared Supabase project (zbmkhvpokopvhnochcjr). Create a migration file under supabase/migrations/ and apply it once.

Convention: Game IDs use snake_case. Season IDs follow {game_id}_{mode_id}_season_{n}. Keep them stable — they appear in frontend constants, RPCs, and result records.
-- supabase/migrations/YYYYMMDDHHMMSS_register_my_game.sql

insert into public.games (id, name, description, is_enabled, created_at, updated_at)
values ('my_game', 'My Game', 'A brief description.', true, now(), now())
on conflict (id) do update
  set name = excluded.name, description = excluded.description, updated_at = now();

insert into public.game_modes (game_id, id, name, min_players, max_players, is_enabled, created_at, updated_at)
values ('my_game', 'casual', 'Casual', 2, 2, true, now(), now())
on conflict (game_id, id) do update
  set is_enabled = true, updated_at = now();

insert into public.leaderboard_seasons (id, game_id, mode_id, name, starts_at, is_active, created_at, updated_at)
values ('my_game_casual_season_1', 'my_game', 'casual', 'Season 1', now(), true, now(), now())
on conflict (id) do update
  set is_active = true, updated_at = now();

Apply it to the remote project:

supabase db push --project-ref zbmkhvpokopvhnochcjr

Verify:

select * from public.games where id = 'my_game';
select * from public.game_modes where game_id = 'my_game';

Step 2 — Install the SDK

npm install @mythicalb/sdk

Define your game constants in one place:

// src/playhub.ts
export const GAME_ID     = 'my_game'
export const MODE_ID     = 'casual'
export const SEASON_ID   = 'my_game_casual_season_1'
export const SUPABASE_URL    = import.meta.env.VITE_SUPABASE_URL
export const SUPABASE_ANON   = import.meta.env.VITE_SUPABASE_ANON_KEY
// src/sdk.ts
import { createMythicalSDK } from '@mythicalb/sdk'
import { GAME_ID, MODE_ID, SEASON_ID, SUPABASE_URL, SUPABASE_ANON } from './playhub'

export const mythical = createMythicalSDK({
  supabase: { url: SUPABASE_URL, anonKey: SUPABASE_ANON },
  game: { id: GAME_ID, modeId: MODE_ID, seasonId: SEASON_ID },
  // Optional: add if your game uses Ardor wallets
  ardor: { hostOrigin: 'https://store.mythicalbeings.io' },
  // Optional: add if your game uses Polygon/MetaMask
  polygon: { provider: window.ethereum }
})

Step 3 — Auth & profile

Every player needs a Play Hub profile before creating or joining sessions. Guests can play immediately; they just cannot save scores or claim rewards.

import { mythical } from './sdk'

// Option A: guest (no signup, loses data on session end)
const user = await mythical.auth.signInAsGuest('Guest Player')
console.log(user.isGuest) // true

// Option B: email magic link (persistent account)
await mythical.auth.signInWithEmail('player@example.com')
// → player receives email, clicks link, session is created
const user = await mythical.auth.getUser()
console.log(user.isGuest) // false

// Always create/fetch profile after auth
const profile = await mythical.profile.getOrCreate()
console.log(profile.id, profile.username)
Pattern: Call getOrCreate() on every app load after auth is restored. It is idempotent — safe to call multiple times.

Step 4 — Sessions

Sessions are the core multiplayer unit. One player creates, another joins, both wait for active status, then the game runs. Finish by recording results.

Host flow

// Create session and share the code with other players
const session = await mythical.sessions.create()
console.log('Share this code:', session.session_code)

// Subscribe to real-time updates
mythical.sessions.subscribe(session.session_id, {
  onSessionChange: (s) => {
    if (s.status === 'active') startGame()
  },
  onParticipantsChange: (participants) => {
    updateLobbyUI(participants)
  }
})

Guest/joiner flow

// Join by code
const session = await mythical.sessions.join('ABC123')

mythical.sessions.subscribe(session.session_id, {
  onSessionChange: (s) => {
    if (s.status === 'active') startGame()
  }
})

Start and finish

// Host marks everyone ready
await mythical.sessions.setReady(session.session_id, true)

// When game ends, record results (non-guests only save to leaderboard)
await mythical.sessions.finish(session.session_id, {
  results: [
    { user_id: winnerId, score: 1500, placement: 1 },
    { user_id: loserId,  score:  800, placement: 2 }
  ]
})

Session statuses

StatusMeaning
waitingCreated, waiting for players to join
activeAll players ready, game is running
completedGame finished, results recorded
cancelledSession ended without results

Step 5 — Leaderboards

Leaderboards are per-game, per-mode, per-season. Guest scores are not recorded.

// Get current season standings
const standings = await mythical.leaderboards.getStandings({
  seasonId: SEASON_ID,
  limit: 10
})
// [{ rank: 1, profile_id, username, score }, ...]

// Get player's own rank
const myRank = await mythical.leaderboards.getPlayerRank({
  seasonId: SEASON_ID,
  profileId: profile.id
})

Step 6 — Wallet rewards (optional)

To claim rewards, a player must have an email account and a linked wallet for the reward chain. The SDK handles the wallet linking flow.

Link a wallet

// Polygon (MetaMask or compatible)
await mythical.wallets.connect('polygon')

// Ardor (via Mythical Store embedded wallet)
await mythical.wallets.connect('ardor')

// Check what's linked
const linked = await mythical.wallets.getLinked()
// [{ chain: 'polygon', address: '0x...' }, ...]

Check and claim rewards

const eligibility = await mythical.rewards.canClaim({ chain: 'polygon' })

if (!eligibility.canClaim) {
  console.log(eligibility.reason)
  // e.g. "email_required" | "wallet_not_linked" | "already_claimed"
  return
}

const claim = await mythical.rewards.claim({
  chain: 'polygon',
  rewardId: 'season_1_top_10'
})
console.log('Claimed:', claim.id)

Step 7 — Game-specific state (optional)

If your game needs to share real-time state beyond what sessions provide (dealt cards, board state, selected units), create a dedicated table prefixed by your game ID.

-- Example: my_game_session_state
create table public.my_game_session_state (
  session_id uuid primary key references public.game_sessions(id),
  board      jsonb,
  turn       int not null default 1,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now()
);

-- RLS: only session participants can read/write
alter table public.my_game_session_state enable row level security;

create policy "participants only" on public.my_game_session_state
  for all using (
    exists (
      select 1 from public.session_participants sp
      where sp.session_id = my_game_session_state.session_id
        and sp.user_id = auth.uid()
    )
  );

Subscribe to state changes via Realtime:

import { supabase } from './supabaseClient'

supabase
  .channel(`game-state:${sessionId}`)
  .on('postgres_changes', {
    event: '*',
    schema: 'public',
    table: 'my_game_session_state',
    filter: `session_id=eq.${sessionId}`
  }, (payload) => {
    applyStateUpdate(payload.new)
  })
  .subscribe()

Pre-launch checklist

  • Migration applied to remote project zbmkhvpokopvhnochcjr
  • Verified games and game_modes rows exist and is_enabled = true
  • Season created in leaderboard_seasons with is_active = true
  • Frontend constants (GAME_ID, MODE_ID, SEASON_ID) match the migration
  • playhub_create_session succeeds in production Supabase
  • playhub_join_session succeeds with a second user
  • Session result recorded after finishing
  • Guest play tested — guest must NOT appear in leaderboard
  • Bot / local training mode does NOT call playhub_create_session or write results
  • If using wallets: wallet linking tested end-to-end with challenge + signature
  • No SQL errors in supabase db lint

Common errors

ErrorLikely causeFix
Game mode is not available. games or game_modes row missing, or is_enabled = false Run the migration and verify with select * from games where id = '...'
FunctionsHttpError on wallet link Edge function is outdated after a schema change supabase functions deploy playhub-link-wallet --project-ref zbmkhvpokopvhnochcjr
Guest score not saved Expected — guests are blocked from permanent results Prompt the player to sign in with email before finishing the session
Session stuck in waiting Test session not cleaned up Call playhub_leave_session or delete the row in dev
unique constraint profile_wallets_chain_address_key Wallet already linked to another profile Inform the player — one wallet per chain per account
Realtime not firing Supabase Realtime disabled on the table, or filter wrong Enable replication on the table in the Supabase dashboard; check the filter syntax

Account levels reference

Level How to reach it What it unlocks What is blocked
Guest auth.signInAsGuest() Play, join sessions, try features Permanent scores, leaderboard, rewards
Email account Magic link via auth.signInWithEmail() Persistent profile, leaderboard, session results On-chain rewards (need wallet)
+ Ardor wallet wallets.connect('ardor') after email login Ardor asset transfers, Ardor rewards Polygon rewards
+ Polygon wallet wallets.connect('polygon') after email login Polygon signatures, Polygon rewards Ardor rewards