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)