Getting Started with Aeion BaaS
Create your first project, set up a leaderboard with anti-cheat validation, configure matchmaking queues with MMR + RD, add virtual currency, and launch your first battle pass — all in Aeion OS.
Create Your First Project
Navigate: BaaS → Projects → New Project
A BaaS project is the top-level container — equivalent to a game or app:
json
{
"name": "Arena Champions",
"slug": "arena-champions",
"description": "Competitive multiplayer arena game",
"settings": {
"requireEmailVerification": true,
"allowAnonymousPlayers": false,
"sessionTimeoutHours": 24
}
}
Each project gets:
- Its own set of API keys (for client authentication)
- Isolated database collections (players, guilds, matches, etc.)
- Independent usage metering (for billing)
- Separate Remote Config values
API keys for your game client:
Project ID: proj_01J8XK4N3P
Client Key: pk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Server Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The client key is safe to embed in a game client. The server key is for server-to-server calls only.
Set Up Authentication
BaaS uses Aeion Auth for player accounts. Players can authenticate via:
- Email/password — standard registration/login
- OAuth — Google, Apple, Facebook, Steam (for game platforms)
- Anonymous — for players who want to try before they sign up (can be linked later)
Anonymous → linked account:
Player starts as anonymous
→ gets anonymous playerId
→ later links Google account
→ anonymous playerId merged into permanent account
→ all progress, currency, inventory transferred
Session management: After login, players get a session token valid for 24 hours (configurable). The game client stores this token and refreshes it on startup.
Create Your First Leaderboard
Navigate: BaaS → Leaderboards → New Leaderboard
json
{
"name": "Global Arena Rankings",
"slug": "global-arena",
"type": "global",
"scoringDirection": "desc",
"tieBreakOrder": "timestamp_asc",
"resetPeriod": "weekly",
"allowUpdates": true,
"onlyBetterScores": true,
"maxEntries": 1000
}
Score submission from your game client:
typescript
// Your game server (or trusted client with server key)
// Score is validated against anti-cheat rules before ranking
const result = await baas.leaderboards.submitScore({
leaderboardId: "global-arena",
userId: playerId,
score: matchScore, // int64, max 9.22e18
displayName: playerName,
metadata: { rank: placement, kills: killCount },
});
// Returns: { entry, isNewRecord, previousScore, rankChange }
// entry.leaderboardId, entry.rank, entry.score, entry.validated, entry.flaggedReason
Anti-cheat validation: Before ranking, the anti-cheat rule engine checks:
rate_limit: Is this player submitting scores too frequently? (windowSeconds + maxCalls scope)impossibility: Is the score a statistical outlier against a configured baseline? (metric/threshold/baseline config, compared via azscoreorratiomode)account_age: Has the account been active long enough to have this score? (thresholdDays)honeypot: Hidden field that only bots fill (bots see all form fields including invisible ones)
If blocked → score submitted but flagged, shown to reviewers in the admin console. Severity critical → autoBanOnCritical: true → immediate account suspension.
Reset: Weekly reset clears all entries every Monday at midnight UTC. resetPeriod can also be daily, monthly, or none (permanent).
Configure Matchmaking Queues
Navigate: BaaS → Matchmaking → New Queue
Queue settings:
json
{
"name": "Ranked Solo Queue",
"slug": "ranked-solo",
"region": "us-east",
"gameMode": "ranked",
"minPlayers": 2,
"maxPlayers": 10,
"teamSize": 1,
"skillDeltaSteps": [50, 100, 200],
"widenTimesSeconds": [0, 30, 90],
"antiSmurfGames": 10,
"defaultStartingMmr": 1500,
"defaultStartingRd": 350,
"priorityBias": "skill",
"ticketHeartbeatSeconds": 5,
"ticketMaxLifetimeSeconds": 300
}
Widen behavior: At time 0, match players within 50 MMR. After 30 seconds, relax to 100 MMR. After 90 seconds, 200 MMR. Players always get placed — if no skill-matched game is found, the pool widens until they are.
Player flow:
```typescript // Client joins queue // Queue stores skillDeltaSteps: [50, 100, 200] // Queue stores widenTimesSeconds: [0, 30, 90] // Tickets carry rd (Rating Deviation) from the player's rating history const { ticket } = await baas.matchmaking.enqueue({ queueId: "ranked-solo", userId: playerId, // rd pulled from the player's existing rating, if any // new users get defaultStartingMmr: 1500, defaultStartingRd: 350 });
// Client polls for match (ticketHeartbeatSeconds: 5, maxLifetimeSeconds: 300) const result = await baas.matchmaking.waitForMatch({ ticketId: ticket.id, timeoutSeconds: 120, });
// result.match contains: // { matchId, teams: [{ teamNumber, tickets: [{ userId, mmr, rd, teamNumber }] }], // allTickets, avgMmr, mmrSpread, region } ```
Parties: For squad matchmaking, all party members enqueue with the same partyId. The matchmaking engine matches the party as a unit — a 4-player party searches as a block against other 4-player parties or solo queues.
Add Virtual Currency
Navigate: BaaS → Currencies → New Currency
json
{
"name": "Gold",
"code": "GOLD",
"description": "Earned through gameplay",
"softCap": null,
"hardCap": null
}
json
{
"name": "Gems",
"code": "GEMS",
"description": "Purchased with real money",
"softCap": null,
"hardCap": 1000000
}
Currencies are open per-project codes, not a fixed soft/hard/premium type — name them whatever fits your game, and set an optional hardCap to prevent unbounded balances.
Grant currency after a match:
```typescript // Server-side (after match ends) // Double-entry ledger — every operation is a balanced debit + credit // No direct balance mutation — only new ledger entries, applied through // optimistic version-checked retries (not a row-level lock) await baas.currency.add({ accountId: winnerId, code: "GOLD", amount: 500, reason: "match_reward", sourceId: matchId, });
// Check balance (re-derived from transactions, not stored) const balance = await baas.currency.getBalance({ accountId: playerId, code: "GOLD", }); // balance: { code: "GOLD", amount: 2500 } ```
Transfer currency between accounts (e.g. a player trade):
typescript
// Deduct from one account, credit another — atomic, all-or-nothing
await baas.currency.transfer({
fromAccountId: playerId,
toAccountId: recipientId,
code: "GOLD",
amount: 10000,
});
Create Items and Inventory
Navigate: BaaS → Items → New Item
json
{
"name": "Lightning Sword",
"slug": "lightning-sword",
"description": "A sword crackling with electric energy",
"rarity": "epic",
"maxStackSize": 1,
"tradeable": true,
"sellable": true,
"sellPrice": { "currency": "soft", "amount": 5000 },
"metadata": {
"attack": 150,
"durability": 100,
"element": "lightning"
}
}
Grant item after achievement:
```typescript // Item holdings, bundles, and trades are all tracked here // Every grant/revoke/consume goes through an atomic transaction pipeline await baas.inventory.grant({ userId: playerId, itemId: "lightning-sword", quantity: 1, source: "achievement", // purchased | granted | traded | found sourceId: achievementId, }); // source tracking: where every item came from (audit trail)
// View inventory (tradeable items only) const inventory = await baas.inventory.list({ userId: playerId, filter: { tradeable: true }, }); ```
Configure Anti-Cheat Rules
Navigate: BaaS → Anti-Cheat → New Rule
Rule 1: Rate limit score submissions:
json
{
"name": "Score Submission Rate Limit",
"ruleType": "rate_limit",
"appliesTo": "score",
"action": "block",
"severity": "high",
"config": {
"windowSeconds": 60,
"maxCalls": 3,
"scope": "leaderboardId"
},
"active": true
}
Rule 2: Impossibility check:
json
{
"name": "Impossible Score Check",
"ruleType": "impossibility",
"appliesTo": "score",
"action": "block",
"severity": "critical",
"autoBanOnCritical": true,
"config": {
"metric": "score",
"mode": "zscore",
"threshold": 5,
"baseline": "globalMean"
},
"active": true
}
Rule 3: Account age gate:
json
{
"name": "Minimum Account Age for Top 100",
"ruleType": "account_age",
"appliesTo": "score",
"action": "flag",
"severity": "low",
"config": {
"thresholdDays": 7
},
"active": true
}
Honeypot: Add a hidden field to leaderboard submissions that only bots fill. Configure as a honeypot rule — any submission with the honeypot filled is flagged as a bot.
Create a Battle Pass
Navigate: BaaS → Battle Passes → New Battle Pass
Configure the pass:
json
{
"name": "Season 1: Origins",
"slug": "season-1",
"season": "2026-S1",
"startsAt": "2026-06-01T00:00:00Z",
"endsAt": "2026-08-31T23:59:59Z",
"premiumPriceCurrency": "hard",
"premiumPriceAmount": 999,
"xpPerChallengeComplete": 100,
"xpPerAchievementUnlock": 50,
"xpPerScoreSubmit": 10,
"autoClaimFreeRewards": true,
"autoClaimPremiumRewards": false
}
Define tiers (XP milestones):
json
[
{
"tier": 1,
"xpRequired": 0,
"freeRewards": [{ "type": "currency", "code": "soft", "quantity": 100 }]
},
{
"tier": 2,
"xpRequired": 500,
"freeRewards": [{ "type": "item", "code": "bronze-sword", "quantity": 1 }]
},
{
"tier": 3,
"xpRequired": 1200,
"freeRewards": [{ "type": "currency", "code": "soft", "quantity": 250 }],
"premiumRewards": [
{ "type": "item", "code": "silver-sword", "quantity": 1 }
]
},
{
"tier": 10,
"xpRequired": 10000,
"premiumRewards": [
{ "type": "item", "code": "lightning-sword", "quantity": 1 }
]
}
]
Player progress:
typescript
// Tiered progression, with XP history tracked by source
// xpHistory tracks: { at, amount, source, sourceId } per grant
// After completing a challenge:
await baas.battlePass.grantXp({
userId: playerId,
passId: "season-1",
amount: 100, // xpPerChallengeComplete
source: "challenge_complete",
sourceId: challengeId,
});
// → XP added to progress.xpHistory
// → currentTier recalculated from cumulative XP vs tier.xpRequired
// → if autoClaimFreeRewards: rewards auto-claimed via the loot service
Set Up Push Notifications
Navigate: BaaS → Push → New Campaign
Configure providers:
json
{
"ios": {
"provider": "apns",
"keyId": "...",
"teamId": "...",
"bundleId": "com.yourgame.ios"
},
"android": { "provider": "fcm", "projectId": "...", "privateKey": "..." }
}
Create a campaign:
json
{
"name": "Season 1 Starting Soon",
"title": "Season 1: Origins launches June 1!",
"body": "Prepare your loadout. New battle pass rewards await.",
"imageUrl": "https://cdn.yourgame.com/season1-banner.webp",
"deepLink": "yourgame://battle-pass/season-1",
"targetSource": { "type": "all" },
"priority": "high",
"respectQuietHours": true,
"mirrorToInbox": true,
"attachedRewards": [{ "type": "currency", "code": "soft", "quantity": 100 }]
}
Send: Players receive the push notification. If they tap it:
- App opens to
yourgame://battle-pass/season-1 - If
mirrorToInbox: true, an in-game inbox message is created - If
attachedRewardsare present, 100 soft currency is granted
Deploying Serverless Functions
Navigate: BaaS → Functions → New Function
```typescript // match-result-processor.ts // Triggered by: match.ended event
export async function handleMatchEnd(input: MatchEndInput, ctx: Aeion) { const { matchId, winnerId, loserId, playerScores } = input;
// Validate scores (anti-cheat)
for (const { userId, score } of playerScores) {
const result = await ctx.anticheat.validate({
projectId: ctx.projectId,
userId,
targetType: "score",
value: score,
resourceKey: "ranked-solo",
});
if (!result.allowed) {
ctx.log("warn", Score flagged for ${userId}: ${result.reason});
}
}
// Update ratings const { winner, loser } = await ctx.ratings.updateFromMatch({ winnerId, loserId, queueId: match.queueId, });
// Grant rewards await ctx.currency.grant({ accountId: winnerId, currency: "soft", amount: 500, reason: "match_reward", sourceId: matchId, });
// Log function output to live clients via WebSocket
ctx.log(
"info",
Match ${matchId}: Winner ${winnerId} (${winner.rating}) beat ${loserId} (${loser.rating}),
);
}
```
A complete game backend. Push notifications (APNs / FCM / Web Push) with campaigns + A/B variants, inventory with items + bundles + trades, in-game inbox with transactional reward delivery, GDPR player-data export + deletion, guilds + guild wars, battle passes with tiered XP progression, tournaments (single-elim / round-robin), achievements, sandboxed JS / TS cloud functions, Glicko-2 matchmaking, rule-based anti-cheat, WebRTC voice with spatial 3D audio, leaderboards, LiveOps, marketplace. Plus double-entry virtual currency.
**BaaS** owns the persistent game-backend domain — players, profiles, currencies, inventories, leaderboards, matchmaking, achievements, tournaments, anti-cheat, battle passes, cloud functions. **Realtime** owns the live-session infrastructure — WebRTC, LiveKit, sessions, AI participants. **Spatial** owns the 3D engine — scenes, physics, WebXR. The 3 modules compose: a multiplayer match uses BaaS to find opponents (Glicko-2), Realtime to host the live session, and Spatial to render the 3D environment.
Yes. Each function runs in an isolated Node.js VM sandbox with a best-effort memory limit (default 128MB) and timeout. Full per-tenant isolation via a WASM runtime is on the roadmap. Functions are transpiled on deploy. Triggers: adhoc, HTTP, cron, event. `console.log` output streams to connected clients via WebSocket for live debugging.
Every currency transaction is a balanced pair: debit one account, credit another, atomically, with optimistic version-checked retries guarding concurrent writes. Each currency code has its own ledger. There is no "balance" field that can be mutated directly — balance is always `SUM(credits) - SUM(debits)` for the player. This means classic dupe patterns (double-spend, race conditions on UPDATE, etc.) are structurally impossible.