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:
| Method | Use Case | Auto UI | Token Type |
|---|---|---|---|
| Developer Token | Development/testing | No | developer |
| Dynamic Developer Token | Development without hardcoding | No | developer |
| Device Auth Flow | Browser games (production) | Yes | player |
| Headless Device Auth | Terminal/CLI apps | No | player |
| Player Token | Backend services | No | player |
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 tokenSecurity: 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:fullinstead ofplayer: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 neededHow It Works:
- SDK detects no valid token
- Shows login modal with "Login to Play" button
- User clicks button, opens PlayKit login page in new tab
- User completes login (email/phone verification)
- SDK polls for completion, receives token
- 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:
| Environment | Default Storage | Persistence |
|---|---|---|
Browser (mode: 'browser') | localStorage | Persistent |
Server (mode: 'server') | In-memory | Session 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:
- Browser mode: SDK automatically shows login UI
- Server mode: Throws
REFRESH_TOKEN_EXPIREDerror
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 tokenSee 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' eventError Handling
Error Codes
| Code | Description | Action |
|---|---|---|
ACCESS_DENIED | User denied authorization | Show message, offer retry |
EXPIRED | Session or token expired | Restart auth flow |
CANCELLED | Auth flow was cancelled | User action, no error needed |
NOT_AUTHENTICATED | No token available | Start login flow |
AUTH_FAILED | Token validation failed | Re-authenticate |
AUTH_ERROR | General authentication error | Check error message |
NO_REFRESH_TOKEN | No refresh token available | Re-authenticate |
REFRESH_TOKEN_EXPIRED | Refresh token has expired | Re-authenticate |
REFRESH_TOKEN_INVALID | Refresh token is invalid | Re-authenticate |
TOKEN_EXPIRED | Access token expired, no refresh available | Re-authenticate |
INSUFFICIENT_CREDITS | Not 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
| Scope | Description | Use Case |
|---|---|---|
player:play | Player-level access, bound to specific game | Production games |
developer:full | Developer-level access, global | Development/testing |
Security Best Practices
- Never expose Developer Tokens in production code - Use environment variables or dynamic auth
- Validate tokens server-side before trusting user identity
- Use HTTPS in production environments
- Handle token expiration gracefully - Listen for
auth_errorevents - Don't store sensitive data alongside tokens in localStorage
- Implement logout on security-sensitive operations
Next Steps
- Backend Services - Server-side token validation and AI calls
- Text Generation - Start generating AI content
- NPC Conversations - Build intelligent game characters