Minecraft Scripting API

Please note: Some examples on this page may be outdated or may not work as expected.

SuperDB - Advanced Usage

Advanced patterns and techniques with SuperDB for complex data management. Once you understand basic storage, you can unlock powerful features like leaderboards, transactions, validation, and analytics - all built on top of simple JavaScript objects and the file system.

How It Works

SuperDB stores plain JavaScript objects as files. Because everything is just JavaScript, you can:

  • Query by iterating through all entries and filtering (like SQL WHERE clause)
  • Sort by converting to arrays, sorting, then returning results
  • Validate by checking structure before saving
  • Cache by keeping frequently accessed data in memory

The key insight: SuperDB is simple by design. Complex features come from writing functions that manipulate the basic get/set operations.

Querying & Filtering

SuperDB doesn't have SQL - instead, you query by iterating through all data and filtering. This is simple but means large databases might be slow. For most use cases (100-1000 players), it's fine.

import { SuperDB } from "@minecraft/server";

const playerDB = new SuperDB({ name: "playerData" });

// **Find all players who meet a condition** (like SQL: WHERE level >= minLevel)
function getPlayersByLevel(minLevel) {
  const results = [];
  // Iterate all stored players
  playerDB.forEach((key, player) => {
    // Test condition and include in results
    if (player.level >= minLevel) {
      results.push({ id: key, ...player }); // Spread operator includes all player properties
    }
  });
  return results;
}

// **Get top N players by a stat** (like SQL: ORDER BY kills DESC LIMIT 10)
function getLeaderboard(limit = 10) {
  const players = [];
  // Load all players into memory
  playerDB.forEach((key, player) => {
    players.push({ id: key, ...player });
  });
  
  // Sort by kills (high to low) and take top N
  return players
    .sort((a, b) => b.stats.kills - a.stats.kills) // Descending order
    .slice(0, limit);
}

// **Find players active in last N hours** (useful for activity tracking)
function getActivePlayersRecently(hours = 24) {
  const timeAgo = Date.now() - (hours * 60 * 60 * 1000); // Convert hours to milliseconds
  const active = [];
  
  playerDB.forEach((key, player) => {
    // Check if last login is after our cutoff time
    if (player.lastLogin > timeAgo) {
      active.push(player);
    }
  });
  
  return active;
}

Transactions & Batch Operations

Transactions are operations that involve multiple steps and need to succeed or fail as a unit. For example, when transferring items between players, you need to remove from one AND add to the other - if it fails halfway, both have changed which is bad. By loading both, making all changes, then saving both, you ensure atomicity.

// **Transfer items between two players** - both must succeed or neither does
function transferItems(fromId, toId, itemId, amount) {
  // Load both player records
  const from = playerDB.get(fromId);
  const to = playerDB.get(toId);
  
  // Validate both players exist
  if (!from || !to) return false;
  
  // **Find the item in sender's inventory**
  const itemIndex = from.inventory.findIndex(
    item => item.id === itemId
  );
  
  // **Check sender has enough items** - validate BEFORE making changes
  if (itemIndex === -1 || from.inventory[itemIndex].amount < amount) {
    return false; // Not enough - fail early, no changes made
  }
  
  // **Remove from sender** - now we know this will work
  from.inventory[itemIndex].amount -= amount;
  if (from.inventory[itemIndex].amount === 0) {
    from.inventory.splice(itemIndex, 1); // Remove empty stack
  }
  
  // **Add to receiver** - merge stacks if item already exists
  const recipientItem = to.inventory.find(
    item => item.id === itemId
  );
  
  if (recipientItem) {
    recipientItem.amount += amount; // Stack exists, just add
  } else {
    to.inventory.push({ id: itemId, amount }); // New item, add to inventory
  }
  
  // **Save both** - now the operation is complete
  playerDB.set(fromId, from);
  playerDB.set(toId, to);
  
  return true; // Success
}

// **Update multiple players at once** - useful for events (double XP weekend, etc.)
function levelUpMultiplePlayers(playerIds) {
  playerIds.forEach(playerId => {
    const player = playerDB.get(playerId);
    if (player) {
      player.level += 1;
      player.experience = 0; // Reset XP on level up
      playerDB.set(playerId, player); // Save each update
    }
  });
  
  // Tip: For huge batches, consider doing this in chunks to avoid lag
}

Schema Validation

Validation ensures data has the right structure before you use it. This is crucial because data can be corrupted, created by old code, or modified incorrectly. Before using stored data, check it's valid.

// **Check if player data has all required fields with correct types**
function isValidPlayerData(data) {
  return (
    typeof data.name === 'string' &&           // Name must exist and be text
    typeof data.level === 'number' &&           // Level must be a number
    typeof data.experience === 'number' &&      // XP must be a number
    Array.isArray(data.inventory) &&            // Inventory must be an array (items)
    typeof data.stats === 'object'              // Stats must be an object (nested data)
  );
}

// **Fix old or corrupted data when format changed**
// Scenario: You added a "stats" field to all players, but old saves don't have it
function migrateDatabase() {
  playerDB.forEach((key, player) => {
    let changed = false;
    
    // **Add missing fields with sensible defaults**
    if (!player.lastUpdated) {
      player.lastUpdated = Date.now(); // When was this player last accessed
      changed = true;
    }
    
    if (!player.stats) {
      // Old data didn't have stats - create empty ones
      player.stats = { kills: 0, deaths: 0 };
      changed = true;
    }
    
    if (!player.achievements) {
      // Old data didn't have achievements - start with none
      player.achievements = [];
      changed = true;
    }
    
    // **Validate the fixed data before saving**
    if (!isValidPlayerData(player)) {
      console.log(`Warning: Player ${key} failed validation after migration`);
      return; // Skip saving if still invalid
    }
    
    if (changed) {
      playerDB.set(key, player); // Save migrated data
    }
  });
}

// **Use this when loading critical data**
const player = playerDB.get(playerId);
if (player && isValidPlayerData(player)) {
  // Safe to use player.level, player.stats, etc.
} else {
  // Data is missing or corrupted
  console.warn(`Invalid player data for ${playerId}`);
}

Economy System

An economy system tracks currency and transactions. By storing transactions in history, you can:

  • Show players their balance
  • Display transaction history
  • Audit fraud or cheating
  • Calculate statistics (top spenders, average income, etc.)
const economyDB = new SuperDB({ name: "economy" });

// **Create a wallet when a new player joins**
function createWallet(playerId, startingBalance = 100) {
  economyDB.set(playerId, {
    balance: startingBalance,           // Current money
    transactions: [],                   // History of all money events
    lastTransaction: null               // When last transaction happened
  });
}

// **Add currency** (earned, reward, admin grant, etc.)
function addBalance(playerId, amount) {
  const wallet = economyDB.get(playerId);
  if (!wallet) return false; // Wallet doesn't exist
  
  // Update balance
  wallet.balance += amount;
  
  // Record in history (for auditing and stats)
  wallet.transactions.push({
    type: "deposit",
    amount,
    timestamp: Date.now()
  });
  wallet.lastTransaction = Date.now();
  
  economyDB.set(playerId, wallet);
  return true;
}

// **Spend currency** (shop purchase, penalty, tax, etc.)
function removeBalance(playerId, amount) {
  const wallet = economyDB.get(playerId);
  if (!wallet || wallet.balance < amount) return false; // Check funds exist
  
  // Deduct
  wallet.balance -= amount;
  
  // Record the withdrawal
  wallet.transactions.push({
    type: "withdrawal",
    amount,
    timestamp: Date.now()
  });
  wallet.lastTransaction = Date.now();
  
  economyDB.set(playerId, wallet);
  return true;
}

// **Check balance** - simpler than storing full wallet if you only need money
function getBalance(playerId) {
  const wallet = economyDB.get(playerId);
  return wallet ? wallet.balance : 0;
}

// **Show recent transactions** - useful for "/history" command
function getTransactionHistory(playerId, limit = 10) {
  const wallet = economyDB.get(playerId);
  if (!wallet) return [];
  
  // Get last N transactions
  return wallet.transactions.slice(-limit);
}

// **Calculate spending** - for achievements like "spend 1000 coins"
function getTotalSpent(playerId) {
  const wallet = economyDB.get(playerId);
  if (!wallet) return 0;
  
  // Sum all withdrawals
  return wallet.transactions
    .filter(t => t.type === "withdrawal")     // Only spending, not earnings
    .reduce((sum, t) => sum + t.amount, 0);   // Add them up
}

Statistics & Analytics

Track achievements and generate stats about individual players or the whole server. This powers leaderboards, profile pages, and progression systems.

// **Unlock achievement** - prevent duplicates by checking if already unlocked
function unlockAchievement(playerId, achievementId) {
  const player = playerDB.get(playerId);
  if (!player) return false; // Player doesn't exist
  
  // Don't unlock twice (maybe player met condition again)
  if (player.achievements.includes(achievementId)) {
    return false; // Already have it
  }
  
  player.achievements.push(achievementId); // Add to list
  playerDB.set(playerId, player);
  return true;
}

// **Get individual player stats** - used for profile page or /stats command
function getPlayerStats(playerId) {
  const player = playerDB.get(playerId);
  if (!player) return null;
  
  // Calculate derived stats from raw data
  const kills = player.stats?.kills || 0;
  const deaths = player.stats?.deaths || 0;
  
  return {
    name: player.name,
    level: player.level,
    kills,
    deaths,
    ratio: kills / Math.max(1, deaths),  // Avoid division by zero
    playtime: player.playtime || 0,
    achievements: player.achievements?.length || 0
  };
}

// **Calculate server-wide stats** - for server dashboard or /info
function getGlobalStats() {
  let totalPlayers = 0;
  let totalKills = 0;
  let totalDeaths = 0;
  let totalHours = 0;
  
  // Iterate all players and sum their stats
  playerDB.forEach((key, player) => {
    totalPlayers++;
    totalKills += player.stats?.kills || 0;
    totalDeaths += player.stats?.deaths || 0;
    totalHours += (player.playtime || 0) / 3600; // Convert seconds to hours
  });
  
  return {
    players: totalPlayers,
    totalKills,
    totalDeaths,
    totalHours: Math.round(totalHours),
    avgKillsPerPlayer: Math.round(totalKills / totalPlayers) || 0 // Prevent NaN
  };
}

Caching & Performance

For frequently accessed data (like a player in-game right now), reading from disk every time is slow. Cache keeps hot data in memory and automatically expires old entries.

// **Wrap SuperDB with a cache layer** - faster reads, smart invalidation
class DatabaseCache {
  constructor(db, ttl = 60000) {
    this.db = db;                      // Underlying database
    this.ttl = ttl;                    // Time before cache entry expires (ms)
    this.cache = new Map();            // In-memory map of cached data
  }
  
  get(key) {
    const cached = this.cache.get(key);
    
    // Check if we have it cached AND it hasn't expired
    if (cached && Date.now() - cached.timestamp < this.ttl) {
      return cached.value; // Return from memory (very fast)
    }
    
    // Cache miss or expired - load from disk
    const value = this.db.get(key);
    this.cache.set(key, { value, timestamp: Date.now() });
    return value;
  }
  
  set(key, value) {
    this.db.set(key, value);           // Write to disk immediately
    this.cache.set(key, {              // Update cache
      value,
      timestamp: Date.now()
    });
  }
  
  invalidate(key) {
    // Force cache refresh next time
    this.cache.delete(key);
  }
  
  clear() {
    // Clear all cached entries
    this.cache.clear();
  }
}

// **Use cached database for frequently accessed players**
const cachedDB = new DatabaseCache(playerDB, 30000); // Keep data fresh for 30 seconds

// Example: Player in-game
// First call: reads from disk, caches
// Second call within 30s: reads from memory (fast)
// Call after 30s: expires, reads from disk again

world.afterEvents.playerJoin.subscribe((event) => {
  const player = event.player;
  // First access in this session - loads from disk
  const data = cachedDB.get(player.id);
});

// When player moves, don't constantly hit disk - use cache
world.afterEvents.entityTick.subscribe((event) => {
  if (event.entity.typeId === "minecraft:player") {
    // Uses cached version if accessed recently
    const playerData = cachedDB.get(event.entity.id);
  }
});

When to use caching:

  • Players logged in right now (access data frequently)
  • Leaderboard data (accessed once per tick to update scores)
  • Configuration data (rarely changes, accessed constantly)

When NOT to use caching:

  • Data that changes frequently and must always be current (use immediate reads instead)
  • Huge datasets (wastes memory)
  • Rarely accessed data (cache overhead not worth it)
Navigation