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.

1

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.

2

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.

3

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 a zscore or ratio mode)
  • 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 criticalautoBanOnCritical: true → immediate account suspension.

Reset: Weekly reset clears all entries every Monday at midnight UTC. resetPeriod can also be daily, monthly, or none (permanent).

4

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.

5

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, });

6

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 }, }); ```

7

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.

8

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

9

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:

  1. App opens to yourgame://battle-pass/season-1
  2. If mirrorToInbox: true, an in-game inbox message is created
  3. If attachedRewards are 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.