Multi-store
Run parallel progression systems with isolated data stores.
The XP plugin supports multiple isolated data stores so you can run parallel progression systems side-by-side. Each store has independent user data, configuration, leaderboard cache, and event stream.
Default store
The default store ('default') is used automatically when no storeId is specified. All built-in commands (/rank, /leaderboard, /xp) operate on the default store.
import { XP } from '@robojs/xp'
// These are equivalent
await XP.addXP(guildId, userId, 100)
await XP.addXP(guildId, userId, 100, { storeId: 'default' })import { XP } from '@robojs/xp'
// These are equivalent
await XP.addXP(guildId, userId, 100)
await XP.addXP(guildId, userId, 100, { storeId: 'default' })Custom stores
Access custom stores by passing { storeId } in the options parameter.
import { XP, leaderboard, config } from '@robojs/xp'
// Reputation store
await XP.addXP(guildId, userId, 50, { storeId: 'reputation' })
const repXP = await XP.getXP(guildId, userId, { storeId: 'reputation' })
// Credits store
await XP.addXP(guildId, userId, 200, { storeId: 'credits' })
const credits = await XP.getXP(guildId, userId, { storeId: 'credits' })
// Per-store leaderboard
const repTop = await leaderboard.get(guildId, 0, 10, { storeId: 'reputation' })
// Per-store config
await config.set(guildId, { cooldownSeconds: 120 }, { storeId: 'reputation' })import { XP, leaderboard, config } from '@robojs/xp'
// Reputation store
await XP.addXP(guildId, userId, 50, { storeId: 'reputation' })
const repXP = await XP.getXP(guildId, userId, { storeId: 'reputation' })
// Credits store
await XP.addXP(guildId, userId, 200, { storeId: 'credits' })
const credits = await XP.getXP(guildId, userId, { storeId: 'credits' })
// Per-store leaderboard
const repTop = await leaderboard.get(guildId, 0, 10, { storeId: 'reputation' })
// Per-store config
await config.set(guildId, { cooldownSeconds: 120 }, { storeId: 'reputation' })Stores are created implicitly on first use. No registration or setup required.
Use cases
| Use case | Default store | Custom stores |
|---|---|---|
| Leveling + currencies | XP/Levels with role rewards | 'coins', 'gems', 'tokens' |
| Multi-dimensional reputation | Overall activity | 'helpfulness', 'creativity', 'trading' |
| Seasonal systems | Permanent progression | 'season1', 'season2', 'event_halloween' |
Multi-currency economy
import { XP } from '@robojs/xp'
// Award to multiple stores simultaneously
await XP.addXP(guildId, userId, 20, { reason: 'message' })
await XP.addXP(guildId, userId, 10, { reason: 'message', storeId: 'coins' })
if (member.roles.cache.has(premiumRoleId)) {
await XP.addXP(guildId, userId, 5, { reason: 'premium_message', storeId: 'tokens' })
}import { XP } from '@robojs/xp'
// Award to multiple stores simultaneously
await XP.addXP(guildId, userId, 20, { reason: 'message' })
await XP.addXP(guildId, userId, 10, { reason: 'message', storeId: 'coins' })
if (member.roles.cache.has(premiumRoleId)) {
await XP.addXP(guildId, userId, 5, { reason: 'premium_message', storeId: 'tokens' })
}Seasonal battle pass
import { XP, config } from '@robojs/xp'
const CURRENT_SEASON = 'season3'
// Award seasonal XP
await XP.addXP(guildId, userId, amount, {
reason: 'seasonal_activity',
storeId: CURRENT_SEASON
})
// Configure seasonal progression (lookup curve with 50 levels)
await config.set(guildId, {
levels: {
type: 'lookup',
params: { thresholds: [0, 100, 250, 500, 1000, 2000, 5000] }
}
}, { storeId: CURRENT_SEASON })import { XP, config } from '@robojs/xp'
const CURRENT_SEASON = 'season3'
// Award seasonal XP
await XP.addXP(guildId, userId, amount, {
reason: 'seasonal_activity',
storeId: CURRENT_SEASON
})
// Configure seasonal progression (lookup curve with 50 levels)
await config.set(guildId, {
levels: {
type: 'lookup',
params: { thresholds: [0, 100, 250, 500, 1000, 2000, 5000] }
}
}, { storeId: CURRENT_SEASON })Store isolation
Each store is fully isolated:
- User data -- XP, levels, messages, and cooldowns are independent
- Configuration -- Each store has its own
GuildConfig(cooldown, xpRate, etc.) - Leaderboard cache -- Caches and invalidation are per-store
- Events -- All events include
storeIdfor filtering - Schema versions -- Migrations run independently per store
Changing one store's config doesn't affect others. Leaderboard invalidation in one store doesn't clear other stores' caches.
Event filtering
All events include a storeId field. Filter events to respond only to specific stores.
import { events } from '@robojs/xp'
events.onLevelUp(({ userId, newLevel, storeId }) => {
if (storeId === 'reputation') {
console.log(`Reputation level up: ${newLevel}`)
} else if (storeId === 'default') {
console.log(`XP level up: ${newLevel}`)
}
})import { events } from '@robojs/xp'
events.onLevelUp(({ userId, newLevel, storeId }) => {
if (storeId === 'reputation') {
console.log(`Reputation level up: ${newLevel}`)
} else if (storeId === 'default') {
console.log(`XP level up: ${newLevel}`)
}
})Constraints
Built-in commands
Built-in commands (/rank, /leaderboard, /xp give, etc.) only interact with the default store. Custom stores require building custom commands.
Role rewards
Role rewards only trigger for the default store. This prevents conflicts where multiple stores would compete over the same roles.
To grant roles based on a custom store, build a custom event listener. See Role rewards for an example.
Data storage
Flashcore namespaces are scoped by store:
xp:default:{guildId}:users:{userId}
xp:reputation:{guildId}:users:{userId}
xp:coins:{guildId}:users:{userId}Each store maintains independent user records, member tracking, and schema versions.
Performance
Each store adds approximately 10KB of memory per guild for the leaderboard cache (100 entries). A guild with 3 stores uses roughly 30KB.
XP changes in one store don't invalidate caches for other stores, keeping cache hit rates high.
