Skip to main content

Persistent Game Data

Save player progress, settings, and scores with cloud or local storage. The Hyve SDK provides flexible persistence that works across devices or locally for offline functionality.

Overview

The persistence system supports two storage backends:

  • Cloud Storage: Syncs across devices, requires authentication
  • Local Storage: Device-specific, works offline, no authentication needed

All data is automatically scoped to your game and the authenticated user (for cloud storage).

Quick Start

import { HyveClient } from '@hyve-sdk/js';

const client = new HyveClient({
storageMode: 'cloud' // Default storage mode
});

// Authenticate (required for cloud storage)
await client.authenticateFromUrl();

// Save data
await client.saveGameData('high_score', 1500);

// Get data
const data = await client.getGameData('high_score');
console.log('High score:', data?.value);

// Delete data
await client.deleteGameData('high_score');

Storage Modes

Comparison

FeatureCloudLocal
Cross-Device Sync✅ Yes❌ No
Requires Authentication✅ Yes❌ No
Works Offline❌ No✅ Yes
Requires game-id✅ Yes✅ Yes
Use CasesProgress, scores, unlocksSettings, preferences

Configuration

Set the default storage mode when creating the client:

// Default to cloud storage
const client = new HyveClient({
storageMode: 'cloud'
});

// Default to local storage
const client = new HyveClient({
storageMode: 'local'
});

Change storage mode at runtime:

// Switch to local storage
client.configureStorage('local');

// Get current mode
const mode = client.getStorageMode(); // 'cloud' or 'local'

Override storage mode per operation:

// Use cloud storage for this operation
await client.saveGameData('score', 1500, 'cloud');

// Use local storage for this operation
await client.saveGameData('settings', {...}, 'local');

Basic Operations

Save Data

Store any JSON-serializable value:

// Numbers
await client.saveGameData('high_score', 1500);
await client.saveGameData('coins', 250);

// Strings
await client.saveGameData('player_name', 'PlayerOne');
await client.saveGameData('current_level', 'forest_temple');

// Booleans
await client.saveGameData('sound_enabled', true);
await client.saveGameData('tutorial_completed', false);

// Arrays
await client.saveGameData('unlocked_levels', [1, 2, 3, 5, 8]);
await client.saveGameData('inventory_items', ['sword', 'shield', 'potion']);

// Objects
await client.saveGameData('player_stats', {
level: 10,
xp: 2500,
health: 100,
mana: 80
});

// Nested objects
await client.saveGameData('game_progress', {
current_chapter: 3,
completed_quests: ['tutorial', 'first_boss', 'hidden_treasure'],
active_quests: [
{ id: 'dragon_slayer', progress: 0.5 },
{ id: 'collect_herbs', progress: 0.8 }
],
achievements: {
speedrun: { unlocked: true, date: '2024-01-15' },
perfectionist: { unlocked: false, progress: 0.6 }
}
});

Get Data

Retrieve saved data with metadata:

const data = await client.getGameData('high_score');

if (data) {
console.log('Value:', data.value); // 1500
console.log('Key:', data.key); // 'high_score'
console.log('Created:', data.created_at); // '2024-01-15T10:30:00Z'
console.log('Updated:', data.updated_at); // '2024-01-15T14:20:00Z'
} else {
console.log('No data found for key: high_score');
}

Response Structure:

interface GameDataItem {
key: string; // Data key
value: GameDataValue; // Your saved value (any JSON type)
created_at: string; // ISO 8601 timestamp
updated_at: string; // ISO 8601 timestamp
}

type GameDataValue =
| string
| number
| boolean
| null
| GameDataValue[]
| { [key: string]: GameDataValue };

Delete Data

Remove data when no longer needed:

// Delete single item
const deleted = await client.deleteGameData('high_score');

if (deleted) {
console.log('High score deleted');
} else {
console.log('High score not found');
}

// Delete with storage mode
await client.deleteGameData('settings', 'local');

Batch Operations

Efficiently handle multiple items at once (recommended for performance):

Batch Save

Save up to 100 items in a single request:

await client.batchSaveGameData([
{ key: 'level_1_score', value: 1000 },
{ key: 'level_2_score', value: 1500 },
{ key: 'level_3_score', value: 2000 },
{ key: 'level_1_stars', value: 3 },
{ key: 'level_2_stars', value: 2 },
{ key: 'level_3_stars', value: 3 }
]);

// With storage mode
await client.batchSaveGameData([
{ key: 'sound_volume', value: 0.8 },
{ key: 'music_volume', value: 0.6 },
{ key: 'graphics_quality', value: 'high' }
], 'local');

Batch Get

Retrieve multiple items at once:

const items = await client.getMultipleGameData([
'level_1_score',
'level_2_score',
'level_3_score'
]);

// Process results
items.forEach(item => {
console.log(`${item.key}: ${item.value} (updated: ${item.updated_at})`);
});

// Map to object
const scores = items.reduce((acc, item) => {
acc[item.key] = item.value;
return acc;
}, {});

console.log(scores);
// { level_1_score: 1000, level_2_score: 1500, level_3_score: 2000 }

Batch Delete

Delete multiple items at once:

const deletedCount = await client.deleteMultipleGameData([
'level_1_score',
'level_2_score',
'level_3_score',
'level_1_stars',
'level_2_stars',
'level_3_stars'
]);

console.log(`Deleted ${deletedCount} items`);

// With storage mode
await client.deleteMultipleGameData([
'old_setting_1',
'old_setting_2'
], 'local');

Complete Examples

Example 1: Game Progress Manager

class ProgressManager {
constructor(client) {
this.client = client;
this.progress = null;
}

async initialize() {
// Load progress from cloud
const data = await this.client.getGameData('game_progress', 'cloud');

if (data) {
this.progress = data.value;
} else {
// Initialize new progress
this.progress = {
level: 1,
xp: 0,
coins: 0,
unlocked_levels: [1],
achievements: []
};
await this.save();
}
}

async save() {
await this.client.saveGameData('game_progress', this.progress, 'cloud');
}

async completeLevel(levelNum, score, stars) {
// Update progress
this.progress.level = Math.max(this.progress.level, levelNum + 1);
this.progress.xp += score;
this.progress.coins += stars * 10;

if (!this.progress.unlocked_levels.includes(levelNum + 1)) {
this.progress.unlocked_levels.push(levelNum + 1);
}

// Save to cloud
await this.save();

// Also save level-specific data
await this.client.batchSaveGameData([
{ key: `level_${levelNum}_score`, value: score },
{ key: `level_${levelNum}_stars`, value: stars },
{ key: `level_${levelNum}_completed_at`, value: new Date().toISOString() }
], 'cloud');
}

async reset() {
// Delete all progress
const keys = [
'game_progress',
...this.progress.unlocked_levels.map(l => `level_${l}_score`),
...this.progress.unlocked_levels.map(l => `level_${l}_stars`)
];

await this.client.deleteMultipleGameData(keys, 'cloud');

// Reinitialize
await this.initialize();
}
}

// Usage
const progress = new ProgressManager(client);
await progress.initialize();
await progress.completeLevel(1, 1500, 3);

Example 2: Settings Manager

class SettingsManager {
constructor(client) {
this.client = client;
this.settings = {
sound: {
enabled: true,
volume: 0.8,
music_volume: 0.6
},
graphics: {
quality: 'high',
fullscreen: false,
vsync: true
},
controls: {
jump: 'space',
move: 'wasd',
sensitivity: 0.5
}
};
}

async load() {
// Load from local storage (works offline)
const data = await this.client.getGameData('settings', 'local');

if (data) {
this.settings = { ...this.settings, ...data.value };
}

this.apply();
}

async save() {
// Save to local storage
await this.client.saveGameData('settings', this.settings, 'local');
}

async updateSound(enabled, volume, musicVolume) {
this.settings.sound = {
enabled,
volume,
music_volume: musicVolume
};

await this.save();
this.apply();
}

async updateGraphics(quality, fullscreen, vsync) {
this.settings.graphics = { quality, fullscreen, vsync };
await this.save();
this.apply();
}

apply() {
// Apply settings to game
// ... implementation ...
}

async reset() {
await this.client.deleteGameData('settings', 'local');
this.settings = this.getDefaults();
await this.save();
}

getDefaults() {
return {
sound: { enabled: true, volume: 0.8, music_volume: 0.6 },
graphics: { quality: 'high', fullscreen: false, vsync: true },
controls: { jump: 'space', move: 'wasd', sensitivity: 0.5 }
};
}
}

// Usage
const settings = new SettingsManager(client);
await settings.load();
await settings.updateSound(true, 0.9, 0.7);

Example 3: High Score Leaderboard

class LeaderboardManager {
constructor(client, levelCount) {
this.client = client;
this.levelCount = levelCount;
}

async saveScore(level, score, stars) {
// Get current high score
const currentData = await this.client.getGameData(`level_${level}_score`, 'cloud');
const currentHighScore = currentData?.value || 0;

// Only save if new high score
if (score > currentHighScore) {
await this.client.batchSaveGameData([
{ key: `level_${level}_score`, value: score },
{ key: `level_${level}_stars`, value: stars },
{ key: `level_${level}_date`, value: new Date().toISOString() }
], 'cloud');

return true; // New high score!
}

return false;
}

async getAllScores() {
// Get all level scores at once
const keys = [];
for (let i = 1; i <= this.levelCount; i++) {
keys.push(`level_${i}_score`, `level_${i}_stars`);
}

const items = await this.client.getMultipleGameData(keys, 'cloud');

// Organize by level
const scores = {};
items.forEach(item => {
const match = item.key.match(/level_(\d+)_(score|stars)/);
if (match) {
const level = match[1];
const type = match[2];

if (!scores[level]) scores[level] = {};
scores[level][type] = item.value;
}
});

return scores;
}

async getTotalScore() {
const scores = await this.getAllScores();
return Object.values(scores).reduce((sum, level) => sum + (level.score || 0), 0);
}

async getTotalStars() {
const scores = await this.getAllScores();
return Object.values(scores).reduce((sum, level) => sum + (level.stars || 0), 0);
}

async resetLevel(level) {
await this.client.deleteMultipleGameData([
`level_${level}_score`,
`level_${level}_stars`,
`level_${level}_date`
], 'cloud');
}

async resetAll() {
const keys = [];
for (let i = 1; i <= this.levelCount; i++) {
keys.push(`level_${i}_score`, `level_${i}_stars`, `level_${i}_date`);
}
await this.client.deleteMultipleGameData(keys, 'cloud');
}
}

// Usage
const leaderboard = new LeaderboardManager(client, 10);
const isHighScore = await leaderboard.saveScore(1, 1500, 3);
const totalScore = await leaderboard.getTotalScore();
const totalStars = await leaderboard.getTotalStars();

Example 4: Mixed Storage Strategy

class GameDataManager {
constructor(client) {
this.client = client;
}

async initialize() {
// Load cloud progress (cross-device sync)
const progressData = await this.client.getGameData('progress', 'cloud');
this.progress = progressData?.value || this.getDefaultProgress();

// Load local settings (device-specific)
const settingsData = await this.client.getGameData('settings', 'local');
this.settings = settingsData?.value || this.getDefaultSettings();

// Load local cache (temporary data)
const cacheData = await this.client.getGameData('cache', 'local');
this.cache = cacheData?.value || {};
}

async saveProgress(data) {
this.progress = { ...this.progress, ...data };
await this.client.saveGameData('progress', this.progress, 'cloud');
}

async saveSettings(data) {
this.settings = { ...this.settings, ...data };
await this.client.saveGameData('settings', this.settings, 'local');
}

async updateCache(key, value) {
this.cache[key] = value;
await this.client.saveGameData('cache', this.cache, 'local');
}

async clearCache() {
this.cache = {};
await this.client.deleteGameData('cache', 'local');
}

getDefaultProgress() {
return {
level: 1,
xp: 0,
coins: 0,
unlocked_content: []
};
}

getDefaultSettings() {
return {
sound_volume: 0.8,
music_volume: 0.6,
graphics_quality: 'high'
};
}
}

// Usage
const gameData = new GameDataManager(client);
await gameData.initialize();

// Save progress to cloud (syncs across devices)
await gameData.saveProgress({ level: 5, xp: 1200 });

// Save settings locally (device-specific)
await gameData.saveSettings({ sound_volume: 0.9 });

// Cache temporary data locally
await gameData.updateCache('tutorial_shown', true);

Storage Limits and Best Practices

Limits

  • Key Length: Maximum 255 characters
  • Value Size: Maximum 1MB per item
  • Batch Size: Maximum 100 items per batch operation
  • Key Format: Alphanumeric, underscores, hyphens (case-sensitive)

Best Practices

1. Use Descriptive Keys

// Good
await client.saveGameData('player_level', 10);
await client.saveGameData('level_3_high_score', 2500);

// Avoid
await client.saveGameData('l', 10);
await client.saveGameData('s3', 2500);
// Good - Single request
await client.batchSaveGameData([
{ key: 'level_1_score', value: 1000 },
{ key: 'level_1_stars', value: 3 },
{ key: 'level_1_time', value: 120 }
]);

// Avoid - Multiple requests
await client.saveGameData('level_1_score', 1000);
await client.saveGameData('level_1_stars', 3);
await client.saveGameData('level_1_time', 120);

3. Use Appropriate Storage Mode

// Cloud: Data that syncs across devices
await client.saveGameData('unlocked_levels', [1, 2, 3], 'cloud');
await client.saveGameData('achievements', ['speedrun'], 'cloud');

// Local: Device-specific or offline data
await client.saveGameData('graphics_quality', 'high', 'local');
await client.saveGameData('control_scheme', 'keyboard', 'local');

4. Handle Errors Gracefully

async function saveWithRetry(key, value, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
await client.saveGameData(key, value);
return true;
} catch (error) {
console.error(`Save attempt ${i + 1} failed:`, error);
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
return false;
}

5. Validate Data Before Saving

function validateScore(score) {
return typeof score === 'number' && score >= 0 && score < 1000000;
}

async function saveScore(score) {
if (!validateScore(score)) {
throw new Error('Invalid score value');
}
await client.saveGameData('high_score', score);
}

6. Use Versioning for Complex Data

const SAVE_VERSION = 2;

async function saveGameState(state) {
const saveData = {
version: SAVE_VERSION,
timestamp: Date.now(),
state: state
};
await client.saveGameData('game_state', saveData);
}

async function loadGameState() {
const data = await client.getGameData('game_state');

if (!data) return null;

// Handle version migrations
if (data.value.version === 1) {
return migrateV1ToV2(data.value.state);
}

return data.value.state;
}

Error Handling

Common Errors

try {
await client.saveGameData('high_score', 1500);
} catch (error) {
if (error.message.includes('game-id is required')) {
console.error('Missing game-id in URL parameters');
// Fallback to local storage or show error to user

} else if (error.message.includes('JWT token')) {
console.error('Authentication required for cloud storage');
// Try local storage instead
await client.saveGameData('high_score', 1500, 'local');

} else if (error.message.includes('Network')) {
console.error('Network error - retrying or using local storage');
// Queue for retry or save locally

} else {
console.error('Unexpected error:', error);
}
}

Fallback to Local Storage

async function saveWithFallback(key, value) {
try {
// Try cloud first
await client.saveGameData(key, value, 'cloud');
return 'cloud';
} catch (error) {
console.warn('Cloud save failed, falling back to local:', error);
// Fallback to local
await client.saveGameData(key, value, 'local');
return 'local';
}
}

Retry Logic

async function retryOperation(operation, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await operation();
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}

// Usage
await retryOperation(() =>
client.saveGameData('high_score', 1500)
);

TypeScript Support

Type Definitions

import {
HyveClient,
GameDataItem,
GameDataValue,
GameDataBatchItem,
SaveGameDataResponse
} from '@hyve-sdk/js';

// Custom game data types
interface PlayerProgress {
level: number;
xp: number;
coins: number;
unlocked_levels: number[];
}

interface GameSettings {
sound_volume: number;
music_volume: number;
graphics_quality: 'low' | 'medium' | 'high';
}

// Type-safe save
async function saveProgress(client: HyveClient, progress: PlayerProgress) {
await client.saveGameData('progress', progress, 'cloud');
}

// Type-safe load with type guard
async function loadProgress(client: HyveClient): Promise<PlayerProgress | null> {
const data = await client.getGameData('progress', 'cloud');

if (!data) return null;

// Type guard
if (isPlayerProgress(data.value)) {
return data.value;
}

return null;
}

function isPlayerProgress(value: any): value is PlayerProgress {
return (
typeof value === 'object' &&
typeof value.level === 'number' &&
typeof value.xp === 'number' &&
typeof value.coins === 'number' &&
Array.isArray(value.unlocked_levels)
);
}

API Reference

Methods

MethodParametersReturnsDescription
saveGameDatakey: string, value: GameDataValue, storage?: 'cloud' | 'local'Promise<SaveGameDataResponse>Save single value
getGameDatakey: string, storage?: 'cloud' | 'local'Promise<GameDataItem | null>Get single value
deleteGameDatakey: string, storage?: 'cloud' | 'local'Promise<boolean>Delete single value
batchSaveGameDataitems: GameDataBatchItem[], storage?: 'cloud' | 'local'Promise<SaveGameDataResponse>Save multiple values (max 100)
getMultipleGameDatakeys: string[], storage?: 'cloud' | 'local'Promise<GameDataItem[]>Get multiple values
deleteMultipleGameDatakeys: string[], storage?: 'cloud' | 'local'Promise<number>Delete multiple values, returns count
configureStoragemode: 'cloud' | 'local'voidChange default storage mode
getStorageModenone'cloud' | 'local'Get current storage mode

Types

type GameDataValue =
| string
| number
| boolean
| null
| GameDataValue[]
| { [key: string]: GameDataValue };

interface GameDataItem {
key: string;
value: GameDataValue;
created_at: string;
updated_at: string;
}

interface GameDataBatchItem {
key: string;
value: GameDataValue;
}

interface SaveGameDataResponse {
success: boolean;
message: string;
}

Troubleshooting

Data Not Saving

// Check authentication
if (!client.hasJwtToken()) {
console.error('No JWT token - cloud storage requires authentication');
}

// Check game-id
if (!client.getGameId()) {
console.error('No game-id in URL parameters');
}

// Try with explicit storage mode
await client.saveGameData('test', 123, 'cloud');

Data Not Loading

const data = await client.getGameData('high_score');

if (!data) {
console.log('Key not found - may not have been saved yet');
// Initialize with default value
await client.saveGameData('high_score', 0);
}

Local Storage Full

try {
await client.saveGameData('large_data', bigObject, 'local');
} catch (error) {
if (error.message.includes('QuotaExceeded')) {
console.error('Local storage full - clear old data');
// Clear cache or old data
await client.deleteMultipleGameData([
'old_cache_1',
'old_cache_2'
], 'local');
}
}

Next Steps