Auth & Storage

Roll your own. Watchtower handles multiplayer — you handle identity and persistence.

What Watchtower Provides

Watchtower gives you real-time multiplayer infrastructure:

  • Rooms — create, join, leave
  • Messaging — broadcast and direct messages
  • Presence — who's online, join/leave events

That's it. No accounts, no databases, no persistence. When a player disconnects, they're gone.

What You Bring

For identity and storage, use your own stack:

  • Auth — user accounts, login, sessions
  • Database — saves, inventories, leaderboards, anything that persists

Your game is a web app. You can use any auth provider and any database that works on the web.

Recommended: Supabase

The easiest path is Supabase. Free tier, 5-minute setup, handles both auth and database.

1. Set up Supabase

npm install @supabase/supabase-js
// lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

2. Auth: Login Flow

import { supabase } from './lib/supabase'

// Sign up
await supabase.auth.signUp({ email, password })

// Login
await supabase.auth.signInWithPassword({ email, password })

// OAuth (Google, Discord, GitHub, etc.)
await supabase.auth.signInWithOAuth({ provider: 'discord' })

// Get current user
const { data: { user } } = await supabase.auth.getUser()

3. Connect to Watchtower with User Identity

import { connect } from '@watchtower-sdk/core'
import { supabase } from './lib/supabase'

// Get authenticated user
const { data: { user } } = await supabase.auth.getUser()

if (!user) {
  window.location.href = '/login'
  return
}

// Connect with their real identity
const room = await connect('game-room', {
  playerId: user.id,
  playerName: user.user_metadata?.name || user.email?.split('@')[0]
})

4. Storage: Save to Your Database

// Save player progress
async function saveProgress(userId: string, level: number, score: number) {
  await supabase
    .from('player_progress')
    .upsert({ user_id: userId, level, score })
}

// Load player progress
async function loadProgress(userId: string) {
  const { data } = await supabase
    .from('player_progress')
    .select('*')
    .eq('user_id', userId)
    .single()
  
  return data
}

// Leaderboard
async function getLeaderboard() {
  const { data } = await supabase
    .from('player_progress')
    .select('user_id, score')
    .order('score', { ascending: false })
    .limit(10)
  
  return data
}

Other Options

Supabase not your thing? Any of these work:

ServiceAuthDatabase
Firebase
Clerk
Auth0
PlanetScale
Turso

Guest / Anonymous Players

Not every game needs accounts. For casual games, generate a local ID:

function getGuestId() {
  let id = localStorage.getItem('guestId')
  if (!id) {
    id = 'guest_' + crypto.randomUUID()
    localStorage.setItem('guestId', id)
  }
  return id
}

const room = await connect('casual-game', {
  playerId: getGuestId(),
  playerName: 'Guest'
})

This gives players a consistent identity across sessions on the same device, without requiring signup. Just remember: no backend means no persistence across devices.

The Pattern

// 1. User logs in (your auth)
const user = await yourAuth.getCurrentUser()

// 2. Connect to Watchtower with their identity
const room = await connect('room', {
  playerId: user.id,
  playerName: user.name
})

// 3. Real-time game state → Watchtower messages
room.broadcast({ x, y, action })

// 4. Persistent data → your database
await yourDB.saveProgress(user.id, { level, score })

Watchtower handles the real-time multiplayer. Your stack handles everything that persists.