ℹ️Principle: Treat your backend like a game server — deterministic, atomic, auditable. Every state change (currency, inventory, ownership) must be logged and reversible.
API Design
Versioned REST + tRPC
All game APIs should be versioned (/api/v1/). Use tRPC for type-safe internal calls. Never expose internal IDs — use UUIDs. Rate-limit every endpoint per user, not just per IP.
Data Integrity
Atomic Transactions Always
Any operation that touches GBux, inventory, or NFT state must be wrapped in a DB transaction. Never do partial updates. Use optimistic locking to prevent race conditions on inventory.
Auth Pattern
JWT + Server Sessions
Short-lived JWTs (15min) with refresh tokens stored in httpOnly cookies. Never store tokens in localStorage. Verify wallet signatures server-side with nonce-based challenges.
Serverless Caution
Know Your Vercel Limits
Vercel functions have 10s/60s execution limits. Long-running jobs (NFT minting, batch rewards) must use queues (Inngest, BullMQ) or Vercel Cron. Never block the main thread for blockchain calls.
Observability
Log Everything Game-Critical
Log every GBux transaction, every NFT mint, every character state change with timestamps, user IDs, and before/after values. Use structured JSON logs. Tools: Axiom, Datadog, or Logtail.
Security
Never Trust the Client
All game logic validation must happen server-side. Players cannot grant themselves GBux, items, or characters. Input sanitize everything. Use Zod schemas at every API boundary.
🗄️ Recommended Tech Stack
| Layer | Recommended | Why | Priority |
| Framework | Next.js 14+ (App Router) | Server Components, API Routes, Edge Functions — unified stack | Core |
| Database | Supabase (Postgres) | Row-level security, realtime, auth built in, works on Vercel | Core |
| ORM | Prisma or Drizzle | Type-safe queries, migration management, game schema evolution | Core |
| Auth | Supabase Auth + NextAuth | Email, wallet, OAuth — covers all player auth types | Core |
| Queue / Jobs | Inngest | Serverless-native, retry logic, great for NFT mint queues | Important |
| Cache | Upstash Redis | Rate limiting, session cache, leaderboards — serverless Redis | Important |
| NFT / Web3 | Crossmint API | Custodial wallets, minting, no-code NFT for players | Important |
| File Storage | Supabase Storage / Cloudflare R2 | Character assets, island maps, NFT metadata images | Important |
| Monitoring | Axiom + Vercel Analytics | Structured logging, error tracking, game analytics | Nice to have |
| Testing | Vitest + Playwright | Unit tests for game logic, E2E for critical flows | Nice to have |
CLIENT LAYER
Web App (Next.js)
Mobile (React Native)
Game Client
↓
API LAYER — Vercel Edge / Serverless Functions
/api/auth
/api/characters
/api/gbux
/api/islands
/api/nfts
↓
DATA LAYER
Supabase Postgres
Upstash Redis
Supabase Storage
↓
EXTERNAL SERVICES
Crossmint Wallets
Crossmint Mint API
Stripe / Payments
Inngest Jobs
↓
BLOCKCHAIN LAYER
Solana (cNFTs)
Polygon (EVM alt)
DATABASE SCHEMA — KEY TABLES
-- Core player account
CREATE TABLE players (
id UUID PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE,
wallet_id TEXT, -- Crossmint custodial
gbux_balance BIGINT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Characters owned by players
CREATE TABLE characters (
id UUID PRIMARY KEY,
player_id UUID REFERENCES players(id),
name TEXT NOT NULL,
class TEXT NOT NULL,
nft_mint_id TEXT, -- on-chain address
metadata JSONB,
is_minted BOOLEAN DEFAULT FALSE
);
-- GBux transaction ledger
CREATE TABLE gbux_transactions (
id UUID PRIMARY KEY,
player_id UUID REFERENCES players(id),
amount BIGINT NOT NULL, -- positive or negative
reason TEXT NOT NULL,
ref_id UUID, -- item, quest, purchase
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Islands
CREATE TABLE islands (
id UUID PRIMARY KEY,
owner_id UUID REFERENCES players(id),
name TEXT,
nft_mint_id TEXT,
tier INT DEFAULT 1,
data JSONB
);
API ROUTE PATTERN
// app/api/gbux/transfer/route.ts
import { auth } from '@/lib/auth'
import { db } from '@/lib/db'
import { z } from 'zod'
const Schema = z.object({
toPlayerId: z.string().uuid(),
amount: z.number().int().positive().max(10000),
reason: z.string().min(1).max(100)
})
export async function POST(req) {
const session = await auth()
if (!session) return unauthorized()
const body = Schema.parse(await req.json())
// Atomic transaction — never partial
await db.transaction(async (tx) => {
await tx.debitPlayer(session.id, body.amount)
await tx.creditPlayer(body.toPlayerId, body.amount)
await tx.logTransaction({ ...body })
})
}
✅Always: validate input with Zod → check auth → atomic DB operation → return minimal response. This is the pattern for every game state mutation.
Step 1
Email / Password Auth
Set up Supabase Auth with email verification. Players get a UUID player ID on signup. Create a players row automatically via database trigger.
Step 2
OAuth (Google/Discord)
Enable OAuth providers in Supabase. Discord is critical for game communities. Link social accounts to the same player ID using a player_accounts join table.
Step 3
Wallet Connect
Allow players to link an external wallet. Use nonce-based signature challenge. Store wallet address in players.external_wallet. Crossmint custodial wallet is auto-created separately.
// lib/auth/wallet-connect.ts — Nonce signature verification
export async function verifyWalletSignature(address, signature, nonce) {
// 1. Fetch nonce from Redis (expires in 5 min)
const storedNonce = await redis.get(`nonce:${address}`)
if (!storedNonce || storedNonce !== nonce) throw new Error('Invalid nonce')
// 2. Verify signature matches wallet address
const recovered = ethers.utils.verifyMessage(nonce, signature)
if (recovered.toLowerCase() !== address.toLowerCase()) throw new Error('Bad sig')
// 3. Delete nonce (one-time use)
await redis.del(`nonce:${address}`)
return true
}
⚠️Critical: Auto-provision a Crossmint custodial wallet for EVERY new player at account creation. Don't wait for them to request it. Call POST /api/2022-06-09/wallets immediately after player row is inserted.
Character Lifecycle
Create → Play → Mint
Characters exist as DB records first. Players can play without minting. When they choose to mint, the character becomes a cNFT on Solana via Crossmint. The nft_mint_id field gets populated. Future trades happen on-chain.
Metadata Standard
Dynamic NFT Metadata
Store character stats in Supabase. NFT metadata URI points to your API endpoint which returns live stats. This lets characters evolve without re-minting. Use /api/metadata/character/[id] as the token URI.
// Character NFT metadata endpoint — /api/metadata/character/[id]/route.ts
export async function GET(req, { params }) {
const character = await db.characters.findUnique({ where: { id: params.id } })
// Returns Metaplex-compatible metadata
return Response.json({
name: character.name,
description: `Grudge Island character — ${character.class}`,
image: `${CDN_URL}/characters/${character.id}/avatar.png`,
external_url: `https://grudgestudio.com/character/${character.id}`,
attributes: [
{ trait_type: 'Class', value: character.class },
{ trait_type: 'Level', value: character.metadata.level },
{ trait_type: 'Island', value: character.metadata.island_name },
],
properties: { category: 'image', creators: [{ address: STUDIO_WALLET }] }
})
}
⚠️GBux is real money equivalent. Treat it like a bank ledger. Every change must be logged. Never update gbux_balance directly — always insert a gbux_transactions row and update balance atomically.
Earn
GBux Sources
Quest completion, island activities, daily rewards, referrals, tournament prizes. Each source has a defined max per day to prevent farming exploits.
Spend
GBux Sinks
Character upgrades, island expansion, cosmetic items, NFT minting fees, marketplace transactions. Sinks prevent inflation — design them deliberately.
Convert
On/Off Ramp
Players can purchase GBux via Stripe. Premium GBux (real money) should be tracked separately from earned GBux for regulatory compliance and refund handling.
// lib/gbux.ts — The ONLY way to modify GBux
export async function awardGbux(playerId, amount, reason, refId?) {
if (amount <= 0) throw new Error('Amount must be positive')
return db.transaction(async (tx) => {
// Check daily earn limit
const todayEarned = await tx.getDailyEarned(playerId)
if (todayEarned + amount > DAILY_EARN_LIMIT) throw new Error('Daily limit reached')
// Insert transaction record first
const txn = await tx.gbuxTransactions.create({
data: { playerId, amount, reason, refId }
})
// Update balance atomically
await tx.players.update({
where: { id: playerId },
data: { gbuxBalance: { increment: amount } }
})
return txn
})
}
export async function spendGbux(playerId, amount, reason, refId?) {
return db.transaction(async (tx) => {
const player = await tx.players.findUnique({ where: { id: playerId } })
if (player.gbuxBalance < amount) throw new Error('Insufficient GBux')
await tx.gbuxTransactions.create({
data: { playerId, amount: -amount, reason, refId }
})
await tx.players.update({
where: { id: playerId },
data: { gbuxBalance: { decrement: amount } }
})
})
}
CROSSMINT SERVER-SIDE WALLETS — AUTO-PROVISION FLOW
// lib/wallets.ts — Create custodial wallet for new player
export async function provisionPlayerWallet(playerId, email) {
const res = await fetch('https://staging.crossmint.com/api/2022-06-09/wallets', {
method: 'POST',
headers: {
'X-API-KEY': process.env.CROSSMINT_SERVER_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
type: 'solana-custodial-wallet',
linkedUser: `email:${email}`
})
})
const { walletAddress } = await res.json()
// Store wallet address on player record
await db.players.update({
where: { id: playerId },
data: { walletId: walletAddress }
})
return walletAddress
}
Why Compressed NFTs
cNFT vs Standard NFT
Compressed NFTs use Solana's state compression. Minting cost drops from ~$2 per NFT to ~$0.0005. Perfect for game items, characters, and islands at scale. Crossmint abstracts all of this.
Minting Strategy
Lazy Minting Pattern
Don't mint on character creation. Mint only when the player requests it or reaches a milestone. Use an Inngest background job so minting failures don't block the player.
// lib/nft/mint-character.ts — Via Crossmint Headless Minting
export async function mintCharacterNFT(characterId) {
const character = await db.characters.findUnique({
where: { id: characterId },
include: { player: true }
})
if (character.isMinted) throw new Error('Already minted')
// Mint compressed NFT to player's custodial wallet
const res = await fetch(`https://www.crossmint.com/api/2022-06-09/collections/${COLLECTION_ID}/nfts`, {
method: 'POST',
headers: {
'X-API-KEY': process.env.CROSSMINT_SERVER_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipient: `solana:${character.player.walletId}`,
metadata: {
name: character.name,
image: `${CDN_URL}/characters/${characterId}/avatar.png`,
description: `Grudge Island — ${character.class}`,
attributes: buildAttributes(character)
},
compressed: true // cNFT flag
})
})
const { id: mintJobId } = await res.json()
// Poll for completion via webhook or Inngest
await db.characters.update({
where: { id: characterId },
data: { mintJobId, isMinted: false } // isMinted set true on webhook
})
}
CROSSMINT WEBHOOK — CONFIRM MINT SUCCESS
// app/api/webhooks/crossmint/route.ts
export async function POST(req) {
const event = await req.json()
if (event.type === 'nft.minted') {
await db.characters.update({
where: { mintJobId: event.data.mintId },
data: {
isMinted: true,
nftMintId: event.data.onChain.mintAddress
}
})
// Notify player via WebSocket or push notification
}
}
01
Foundation — Auth & Accounts
Get the player account system fully operational before everything else. This is the bedrock all other systems depend on.
Supabase project setup
Email auth + OAuth (Discord/Google)
Player DB schema deployed
Crossmint wallet auto-provision on signup
Session management (JWT + httpOnly cookies)
Player profile API
DO NOW
02
GBux Ledger System
Implement the virtual currency engine with full transaction logging. No earning or spending until this is bulletproof.
gbux_transactions table
awardGbux / spendGbux atomic functions
Daily earn limits + anti-cheat rules
Balance read API
Transaction history API
DO NOW
03
Characters — Off-Chain First
Characters as full DB entities with all game data. No blockchain yet — get the gameplay loop working first.
Character creation API
Character progression / stats
Character inventory system
Character ↔ Island binding
Dynamic metadata endpoint
NEXT
04
Islands — Off-Chain First
Island creation, ownership, and gameplay data. Islands are the core game world — get them stable before NFT-ing them.
Island creation + ownership
Island tier system
GBux earn mechanics on islands
Island → Character activity
NEXT
05
Crossmint Collection Setup
Create the on-chain collection on Crossmint. Set royalties, set studio wallet as creator. Test mint pipeline on Devnet.
Create Character Collection (Crossmint dashboard)
Create Island Collection
Set royalty % and creator wallet
Devnet test mint → verify metadata
Webhook endpoint for mint events
PHASE 2
06
NFT Minting — Characters & Islands
Enable the mint flow for players. Characters become cNFTs. Islands become cNFTs. All via Crossmint headless API with Inngest job queue.
Inngest mint job setup
Character mint API endpoint
Island mint API endpoint
Mint status polling + webhook confirm
GBux cost to mint
Player mint history UI
PHASE 2
07
Marketplace & Trading
Enable players to trade characters and islands. GBux marketplace for items. NFT marketplace integration for on-chain trades.
GBux item marketplace
Character/Island listing
NFT transfer via Crossmint
Royalty enforcement on resales
PHASE 3
SECURITY
- 🔒
Env vars in Vercel — no secrets in code. CROSSMINT_SERVER_KEY, DB_URL, etc.
- 🔒
Supabase RLS enabled — row-level security on ALL game tables
- 🔒
API rate limiting — Upstash Redis rate limiter on every endpoint
- 🔒
Input validation — Zod schema on every POST/PATCH body
- 🔒
Webhook verification — validate Crossmint webhook signatures
DATA INTEGRITY
- 🗄️
DB migrations tracked — Prisma or Drizzle migration history in git
- 🗄️
Backups configured — Supabase daily backups enabled
- 🗄️
GBux ledger tested — concurrent spend/earn stress tested
- 🗄️
Atomic transactions — no GBux update without a transaction log entry
WEB3 READINESS
- ⛓️
Crossmint staging tested — full mint → webhook → DB update flow verified
- ⛓️
Wallet auto-provision — every new account gets a wallet immediately
- ⛓️
Collection IDs stored — Character and Island collection addresses in env vars
- ⛓️
Metadata endpoint live — /api/metadata/character/[id] returns valid JSON
OPERATIONS
- 📊
Error monitoring — Vercel logs + Axiom or Sentry configured
- 📊
Inngest dashboard — job queue visible, failed jobs alerting
- 📊
Vercel preview deploys — all PRs get a preview URL
- 📊
Staging environment — separate Crossmint staging + Supabase project