PlayKit.ai

Authentication

Understanding PlayKit SDK authentication methods and login flows

Authentication

This guide covers all authentication methods available in the PlayKit SDK, from development to production, and from browser to terminal environments.

Overview

PlayKit SDK supports multiple authentication methods depending on your use case:

MethodUse CaseAuto UIToken Type
Developer TokenDevelopment/testingNodeveloper
Dynamic Developer TokenDevelopment without hardcodingNodeveloper
Device Auth FlowBrowser games (production)Yesplayer
Headless Device AuthTerminal/CLI appsNoplayer
Player TokenBackend servicesNoplayer

Developer Token

For development and testing, use a Developer Token. This skips all login UI and uses your developer account directly.

Static Developer Token

Get your token from the playkit.ai dashboard and use it directly:

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  developerToken: 'your-dev-token',  // From playkit.ai dashboard
});

await sdk.initialize();
// SDK is ready, using developer token

Security: Never commit Developer Tokens to version control or use them in production. Remove developerToken before deploying.

The SDK shows a red "DeveloperToken" indicator when using developer tokens in browser environments.

Dynamic Developer Token

Instead of hardcoding a developer token, you can dynamically obtain one at game startup using the Device Auth Flow with the developer:full scope. This allows developers to test their games without exposing tokens in source code.

How It Works:

  • Scope: developer:full instead of player:play
  • No game_id: Developer tokens are global (not bound to a specific game)
  • X-Game-Id header: Required when making API calls with a global developer token
const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  mode: 'server',
});

// Step 1: Initiate device auth with developer scope
const { authUrl, sessionId, codeVerifier, expiresIn } = await sdk.initiateLogin({
  scope: 'developer:full',
});

// Step 2: Open browser for authorization
console.log('Please visit this URL to authorize:');
console.log(authUrl);

// Step 3: Poll for completion
const result = await sdk.completeLogin(sessionId, codeVerifier, {
  timeoutMs: expiresIn * 1000,
});

console.log('Developer token obtained:', result.access_token);

// Step 4: Reinitialize SDK with the developer token
const devSdk = new PlayKitSDK({
  gameId: 'your-game-id',
  developerToken: result.access_token,
});

await devSdk.initialize();

Development Environment Check

Add this safety net to warn if developer tokens are accidentally used in production:

const isDevelopment = window.location.protocol === 'file:' ||
                      window.location.hostname === 'localhost' ||
                      window.location.hostname === '127.0.0.1';

if (typeof window !== 'undefined' && developerToken && !isDevelopment) {
  alert('Warning: Developer token detected in production!');
}

Player Authentication

For production games, players authenticate via the Device Auth Flow.

Device Auth Flow (Browser)

For production browser games, omit the developerToken. The SDK automatically shows a login modal when the user needs to authenticate.

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  // No developerToken = auto shows login UI
});

sdk.on('authenticated', (authState) => {
  console.log('User logged in!', authState);
});

sdk.on('unauthenticated', () => {
  console.log('User needs to log in');
});

await sdk.initialize();  // Shows login UI if needed

How It Works:

  1. SDK detects no valid token
  2. Shows login modal with "Login to Play" button
  3. User clicks button, opens PlayKit login page in new tab
  4. User completes login (email/phone verification)
  5. SDK polls for completion, receives token
  6. Token is saved locally for future sessions

Customizing the Flow:

const authManager = sdk.getAuthManager();
await authManager.startDeviceAuthFlow({
  scope: 'player:play',
  openBrowser: (url) => {
    window.open(url, 'playkit-login', 'width=500,height=600');
  },
  onAuthUrl: (url) => {
    console.log('Auth URL:', url);
  },
  onPollStatus: (status) => {
    console.log('Poll status:', status);
  },
});

Headless Device Auth (Terminal/CLI)

For terminal applications, CLI tools, or environments without browser popups, use headless device auth.

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  mode: 'server',
});

// Step 1: Get the auth URL
const { authUrl, sessionId, codeVerifier, expiresIn } = await sdk.initiateLogin();

// Step 2: Display URL to user
console.log('Please visit this URL to log in:');
console.log(authUrl);
console.log(`\nThis link expires in ${Math.floor(expiresIn / 60)} minutes.`);

// Or generate a QR code
// const qr = require('qrcode-terminal');
// qr.generate(authUrl, { small: true });

// Step 3: Poll for completion
try {
  const result = await sdk.completeLogin(sessionId, codeVerifier, {
    onStatus: (status) => {
      if (status === 'pending') {
        process.stdout.write('.');
      }
    },
    timeoutMs: expiresIn * 1000,
  });

  console.log('\nLogin successful!');
  console.log('Token:', result.access_token);
} catch (error) {
  if (error.code === 'ACCESS_DENIED') {
    console.log('\nUser denied authorization');
  } else if (error.code === 'EXPIRED') {
    console.log('\nSession expired, please try again');
  }
}

Cancel Ongoing Login:

sdk.cancelLogin();

Token Management

Understanding how tokens are stored, expire, and refresh is important for production applications.

Token Storage

The SDK provides a cross-platform storage abstraction that automatically adapts to the runtime environment:

EnvironmentDefault StoragePersistence
Browser (mode: 'browser')localStoragePersistent
Server (mode: 'server')In-memorySession only

Storage Format:

// Storage key format
`playkit_{gameId}_auth`

// Stored data structure (AES-128-GCM encrypted in browser)
{
  isAuthenticated: boolean;
  token: string;
  tokenType: 'developer' | 'player';
  expiresAt: number;  // Unix timestamp in milliseconds
}

Custom Storage Provider:

Implement the IStorage interface to use custom storage backends (Redis, database, etc.):

import { PlayKitSDK, IStorage } from 'playkit-sdk';

class RedisStorage implements IStorage {
  private client: RedisClient;

  getItem(key: string): string | null {
    return this.client.get(key);
  }

  setItem(key: string, value: string): void {
    this.client.set(key, value);
  }

  removeItem(key: string): void {
    this.client.del(key);
  }

  keys(): string[] {
    return this.client.keys('playkit_*');
  }
}

// Usage with custom storage
const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  playerToken: token,
  mode: 'server',
});

await sdk.initialize();

In server mode, tokens are stored in memory by default and do not persist across process restarts. For production backends, pass tokens directly via playerToken configuration or implement a custom storage provider.

Token Expiration

Player tokens have a default expiration of 24 hours. Refresh tokens are valid for 30 days.

Check Expiration:

// Check if access token is expired
if (sdk.isTokenExpired()) {
  console.log('Access token has expired');
}

// Get expiration time
const authState = sdk.getAuthState();
if (authState.expiresAt) {
  const expiresIn = authState.expiresAt - Date.now();
  console.log(`Access token expires in ${Math.floor(expiresIn / 1000 / 60)} minutes`);
}

// Check refresh token availability
if (authState.refreshToken) {
  console.log('Refresh token available');
}

Token Refresh

The SDK supports token refresh using refresh tokens obtained during authentication.

Browser Mode (Automatic):

In browser mode, the SDK checks token validity before API calls. This is a lightweight timestamp comparison with no performance impact. Only when the token expires within 5 minutes does the SDK make a refresh API call.

// SDK checks: is token expiring soon?
// - If no: proceeds immediately (no latency)
// - If yes: refreshes first, then proceeds
const response = await chatClient.chat('Hello');

Manual Refresh:

For explicit control or server-side applications:

// Check if refresh is possible
if (sdk.canRefreshToken()) {
  const result = await sdk.refreshToken();
  console.log('New token expires in:', result.expiresIn, 'seconds');
}

Server Mode:

In server mode, automatic refresh is disabled. Manage token refresh explicitly:

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  mode: 'server',
  playerToken: storedToken,
});

await sdk.initialize();

// Refresh manually when needed
if (sdk.isTokenExpired() && sdk.canRefreshToken()) {
  const result = await sdk.refreshToken();
  // Store new tokens for future use
  saveTokens(result.accessToken, result.refreshToken);
}

Refresh Events:

// Listen for successful token refresh
sdk.on('token_refreshed', (authState) => {
  console.log('Token refreshed, new expiry:', new Date(authState.expiresAt));
});

Automatic Re-authentication

When token refresh fails or refresh token expires:

  1. Browser mode: SDK automatically shows login UI
  2. Server mode: Throws REFRESH_TOKEN_EXPIRED error
sdk.on('auth_error', (error) => {
  if (error.code === 'REFRESH_TOKEN_EXPIRED') {
    console.log('Session expired, please re-authenticate');
  }
});

Manual Re-authentication

Force re-authentication when needed:

await sdk.logout();
await sdk.startAuthFlow('device');

Token Validation

Validate tokens server-side before trusting them:

// Lightweight validation (no side effects)
const result = await sdk.verifyToken();
console.log(result);
// { valid: true, userId: '...', tokenType: 'player', expiresAt: 1234567890 }

// Full validation (also triggers daily credits refresh)
const playerInfo = await sdk.getPlayerInfo();
console.log(playerInfo);
// { userId, gameId, balance, dailyRefresh, ... }

Backend Services

For backend services that need to make AI calls on behalf of authenticated users:

Using Player Token

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  playerToken: userTokenFromRequest,  // Token from frontend
  mode: 'server',
});

await sdk.initialize();

const chat = sdk.createChatClient('gpt-4o-mini');
const response = await chat.chat('Hello');

Using JWT Exchange

If you have your own authentication system, exchange your JWT for a player token:

const sdk = new PlayKitSDK({
  gameId: 'your-game-id',
  playerJWT: yourSystemJWT,  // Your backend-signed JWT
  mode: 'server',
});

await sdk.initialize();
// SDK automatically exchanges JWT for player token

See Backend Services for complete backend integration guide.

Authentication State

Check Status

// Check if authenticated
if (sdk.isAuthenticated()) {
  console.log('User is logged in');
}

// Get current token
const token = sdk.getToken();

// Get full auth state
const authState = sdk.getAuthState();
console.log(authState);
// { isAuthenticated: true, token: '...', tokenType: 'player', expiresAt: 1234567890 }

Events

// Successful authentication
sdk.on('authenticated', (authState) => {
  console.log('Authenticated:', authState.tokenType);
});

// Session ended
sdk.on('unauthenticated', () => {
  console.log('User logged out or session expired');
});

// Authentication error (token invalid, expired, etc.)
sdk.on('auth_error', (error) => {
  console.error('Auth error:', error.message, error.code);
});

// Token refreshed (access token renewed via refresh token)
sdk.on('token_refreshed', (authState) => {
  console.log('Token refreshed, new expiry:', authState.expiresAt);
});

// Daily credits refreshed (happens on first API call each day)
sdk.on('daily_credits_refreshed', (data) => {
  console.log(`Added ${data.amountAdded} credits!`);
});

Logout

await sdk.logout();
// Clears token, emits 'unauthenticated' event

Error Handling

Error Codes

CodeDescriptionAction
ACCESS_DENIEDUser denied authorizationShow message, offer retry
EXPIREDSession or token expiredRestart auth flow
CANCELLEDAuth flow was cancelledUser action, no error needed
NOT_AUTHENTICATEDNo token availableStart login flow
AUTH_FAILEDToken validation failedRe-authenticate
AUTH_ERRORGeneral authentication errorCheck error message
NO_REFRESH_TOKENNo refresh token availableRe-authenticate
REFRESH_TOKEN_EXPIREDRefresh token has expiredRe-authenticate
REFRESH_TOKEN_INVALIDRefresh token is invalidRe-authenticate
TOKEN_EXPIREDAccess token expired, no refresh availableRe-authenticate
INSUFFICIENT_CREDITSNot enough balance (402)Show upgrade prompt

Example

try {
  await sdk.initialize();
} catch (error) {
  switch (error.code) {
    case 'ACCESS_DENIED':
      showMessage('Please authorize to play.');
      break;
    case 'NOT_AUTHENTICATED':
      console.error('Please provide a token');
      break;
    case 'AUTH_FAILED':
      showMessage('Session expired. Please login again.');
      await sdk.startAuthFlow('device');
      break;
    default:
      console.error('Initialization failed:', error);
  }
}

Scopes Reference

ScopeDescriptionUse Case
player:playPlayer-level access, bound to specific gameProduction games
developer:fullDeveloper-level access, globalDevelopment/testing

Security Best Practices

  1. Never expose Developer Tokens in production code - Use environment variables or dynamic auth
  2. Validate tokens server-side before trusting user identity
  3. Use HTTPS in production environments
  4. Handle token expiration gracefully - Listen for auth_error events
  5. Don't store sensitive data alongside tokens in localStorage
  6. Implement logout on security-sensitive operations

Next Steps