Skip to content

Architecture: State & data

Principles

  • Server owns canonical state.
  • Persistence is versioned and migratable.
  • All grants are idempotent.

Data types

  • Durable (persistent): player progression, inventory, MMR, punishments
  • Ephemeral (short-lived): matchmaking queues, temporary tokens, rate limit counters
  • Observability: events/logs, match summaries, moderation evidence links

Storage strategy

Roblox-side:

  • DataStore: durable player profile and ledgers (careful with budgets)
  • MemoryStore: queues/tokens/state caches (TTL, best effort)
  • MessagingService: fanout invalidations (best effort)

Web-side (dashboard):

  • MySQL/MariaDB for audit logs and config history
  • Optional: Redis for queues/caching

DataStore reliability patterns

Session locking

To prevent data corruption from multiple servers writing to the same player profile:

  • Use session locking pattern: one server "owns" a player's data at a time
  • On join: acquire lock (write server ID + timestamp to profile metadata)
  • On leave: release lock and save final state
  • On conflict: newer session wins after grace period

Consider using @rbxts/profileservice or similar battle-tested library that implements this pattern.

Retry policy

DataStore operations can fail due to throttling or transient errors:

const RETRY_CONFIG = {
  maxAttempts: 3,
  baseDelayMs: 1000,
  maxDelayMs: 8000,
  jitter: true, // ±20% randomization to avoid thundering herd
};

// Exponential backoff: 1s → 2s → 4s (with jitter)

Budget management

DataStore has strict rate limits (60 + 10×players requests/min for GetAsync, etc.):

  • Batch reads on player join (single GetAsync for full profile)
  • Debounce writes (queue changes, flush periodically or on leave)
  • On budget exhaustion: queue writes in-memory, flush on player leave
  • Critical writes (purchases): retry with higher priority, alert if persistently failing

Write-behind queue

interface WriteQueue {
  pending: Map<string, ProfileData>;
  flushIntervalMs: 30000; // flush every 30s
  flushOnLeave: true;
  maxQueueSize: 100; // per server
}

If a player leaves before flush completes, attempt synchronous save with timeout.

Corruption recovery

  • Store schemaVersion and lastWriteAt in every profile
  • On load, validate schema; if corrupt, load from backup or quarantine
  • See docs/runbooks/data-corruption.md for recovery procedures

Profile schema

Implemented across @broblox/data, @broblox/progression, @broblox/inventory, @broblox/rewards, and @broblox/moderation:

  • schemaVersion
  • progression (xp, level, prestige) — @broblox/progression
  • mmr (per mode) — @broblox/matchmaking
  • inventory (owned items, equipped loadouts) — @broblox/inventory
  • moderation (ban state, mutes, trust score) — @broblox/moderation
  • receipts / grants (idempotency keys) — @broblox/rewards
  • quests (active quests, objective progress) — @broblox/quests
  • pets (owned pets, equipped slots, XP) — @broblox/pets
  • cosmetics (owned cosmetics, equipped slots) — @broblox/cosmetics
  • battlePass (season, tier, XP, claims) — @broblox/battle-pass
  • codes (redeemed code IDs) — @broblox/codes
  • marketplace (receipt dedup keys, pass ownership cache) — @broblox/marketplace

Idempotency (non-negotiable)

Every mutation that can be retried must have a unique id:

  • purchases: receipt id
  • rewards: claim id
  • admin actions: action id
  • match results: match id + version

Migration model

  • Each document stores schemaVersion.
  • On load:
  • migrate to current
  • write back only when safe

Schema versioning (BasePlayerStore)

All player stores that extend BasePlayerStore must use TData extends VersionedData. This enforces:

  1. Default data must include __version: 1.
  2. When any stored field is added, renamed, or removed, increment schemaVersion() and implement migrate(data, fromVersion).
  3. On load(), if the stored version is older than schemaVersion(), the store calls migrate(), stamps the new version, and marks dirty to trigger a save.
  4. Data saved with no __version field is treated as version 0.

See ADR-0010 for the full rationale.

Competitive integrity

  • Match results are computed server-side.
  • Rewards are granted via a ledger-like process.
  • Any detected corruption triggers containment (disable grants, require review).