Developer Docs

Game official site & top-up center — design logic, cross-team integration guide and ops deployment. For middleware, game dev, frontend and ops colleagues; jump to the relevant section by role.

Next.js 15
SQLite
Docker
Current: MVP / mock integration

Overview

A game official site + per-product top-up platform: players pick a game, log into their game account (fetch characters), choose a recharge item/amount and pay. Admin can visually configure games and items and view orders.

This is an MVP / demo: game login and payment are mocked (any account/password works in demo, payment succeeds directly) to get the whole flow and UI running first. To go live, only replace the game-backend and payment integrations — frontend and order logic need no changes.

Design Logic

The top-up flow is a clear state chain, helping teams align boundaries:

Player
  │
  ├─ 1. Select game ──────────► GET  /api/games
  │
  ├─ 2. Log in to game account ► POST /api/auth/login   ← game-dev integration point
  │       returns: the account's character list
  │
  ├─ 3. Select recharge item ─► GET  /api/recharge-items?gameSlug=
  │
  ├─ 4. Place order ──────────► POST /api/orders        (server validates amount, tamper-proof)
  │
  └─ 5. Pay ──────────────────► POST /api/orders/:id/pay ← payment integration point
                                (mock: succeeds directly; prod: gateway callback)

Principles: amount is authoritative from the server DB (re-validated on order creation; front-end price is display-only); game login state is held by the game backend (the mock returns characters in plaintext to get running — in production the backend should hold a token and not keep the password on the front end).

Tech Architecture

  • Next.js 15 (App Router) + React 19 + TypeScript (strict)
  • Tailwind CSS v4 + shadcn/ui (Radix) + Lucide icons
  • better-sqlite3: single-file database, zero external deps
  • next-themes (dark mode), sonner (toast), zod (validation)

Directory structure (files most often changed for integration are marked)

src/
├── app/
│   ├── page.tsx                # Home (carousel + intro + game entry)
│   ├── recharge/page.tsx       # Recharge wizard (client-side multi-step)
│   ├── admin/                  # Admin login + protected dashboard
│   └── api/                    # All API routes
├── components/{ui,site,recharge,admin}/
├── lib/
│   ├── db.ts                   # SQLite: tables/seed/CRUD
│   ├── game-service.ts         # ★ game backend integration (mock, replaceable)
│   └── auth.ts                 # admin session auth
└── types/                      # Shared types (GameAccount / Order etc.)

Integration Guide

Jump to the section for your role: game dev → backend integration; middleware/payment → payment; frontend → extend fields.

Game Dev: integrate game backend

Only replace the body of loginToGame() in src/lib/game-service.ts, keeping the input/output types unchanged — no front-end or order logic changes needed.

Contract (front-end/back-end agreement, do not change field names)

// Input
{ gameSlug: string; username: string; password: string }

// Returns GameAccount (types/index.ts)
interface GameCharacter { id: string; name: string; server: string; level: number }
interface GameAccount { accountId: string; accountName: string; characters: GameCharacter[] }

Reference implementation (real backend)

// src/lib/game-service.ts
export async function loginToGame(gameSlug: string, username: string, password: string): Promise<GameAccount> {
  const base = process.env.GAME_API_BASE; // e.g. https://game-api.internal
  const res = await fetch(base + "/login", {
    method: "POST",
    headers: { "x-api-key": process.env.GAME_API_KEY! },
    body: JSON.stringify({ gameSlug, username, password }),
  });
  if (!res.ok) throw new Error("Invalid account or password");
  const data = (await res.json()) as GameAccount;
  // Recommendation: hold the session with the returned token server-side; do not keep plaintext passwords on the client
  return data;
}

Security: in production, account/password should not stay in the browser; the backend should issue a short-lived token after login, and subsequent character queries/orders carry the token, verified by the game backend.

Middleware / Payment: integrate payment

Only one file to change: src/app/api/orders/[id]/pay/route.ts. Order state machine: pending → paid | failed.

// Two ways to integrate real payment (pick one)
// A. Sync cashier: recharge wizard handlePay() redirects to the real payment link (WeChat/Alipay JSAPI),
//    after success the frontend polls GET /api/orders/:id to get the paid status.
//
// B. Async callback (recommended, most robust): the payment gateway notifies this endpoint asynchronously;
//    after signature verification, the order is marked paid.
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  // TODO: verify gateway notify params (Alipay sign / WeChat sign / Stripe webhook secret)
  // if (!verifyGatewayNotify(_req)) return new Response("fail");
  const order = updateOrderStatus(id, "paid");
  return Response.json({ order }); // adjust return as the gateway requires
}

Merchant ID and secrets stay server-side only, never sent to the front end. The async callback URL must be configured as a public HTTPS domain in the merchant console.

Frontend: extend fields

Common extensions (e.g. server/region selection, coupons, invoices) — add the field in types/index.ts, then sync in lib/db.ts schema/CRUD, the API route, and the wizard form. Queries are parameterized, so adding fields is low-effort.

Payment & FX Architecture (Direct Web Top-up)

This platform uses the "direct web top-up" model: users recharge on the official site, bypassing Apple/Google IAP. The base currency is fixed as CNY; the actual local-currency charge and FX conversion are done entirely by the payment processor. The site only shows a reference price.

FX responsibility split

  • Site: only labels the base price (CNY), shows a local-currency reference by language/payment method, and marks it "exchange rate for reference only".
  • Processor (Stripe / Alipay Global / WeChat Cross-border / local wallets): performs Dynamic Currency Conversion (DCC) at the cashier, charges the user in local currency at the settlement rate, and settles to you in the agreed currency.
  • You: avoid the 30% Apple/Google cut, but must own FX-risk handling and local digital-tax (VAT etc.) compliance yourself.

Processor routing

The backend picks the processor by payMethod and passes the display currency as presentmentCurrency:

// payMethod -> processor + presentmentCurrency
const ROUTING: Record<string, { provider: string; currency: string }> = {
  wechat:   { provider: "wechat",   currency: "CNY" },  // CN-only channels
  alipay:   { provider: "alipay",   currency: "CNY" },
  unionpay: { provider: "unionpay", currency: "CNY" },
  applepay: { provider: "stripe",   currency: "DISPLAY" }, // processor does DCC
  googlepay:{ provider: "stripe",   currency: "DISPLAY" },
  simulate: { provider: "mock",     currency: "DISPLAY" },
  // Local wallets: charge in local currency directly, no CNY
  momo:     { provider: "momo",     currency: "VND" },
  truemoney:{ provider: "truemoney",currency: "THB" },
};

Local wallets vs DCC

  • Real local users in SEA mostly use local wallets (MoMo in VN, TrueMoney in TH, GoPay/OVO in ID) — they price and settle natively in local currency, with no CNY involved.
  • When going through Stripe / PayPal / Apple Pay web, the processor does DCC dynamically — showing a reference price is enough.
  • Routing rule: WeChat/Alipay → CNY; Stripe/PayPal → display currency (DCC); local wallet → local currency directly (no CNY).

Backend integration example

In /api/orders/[id]/pay, route to the processor by payMethod:

// In /api/orders/[id]/pay, route to the processor by payMethod
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const { payMethod, displayCurrency } = await req.json();
  const r = ROUTING[payMethod];
  const chargeCurrency = r.currency === "DISPLAY" ? displayCurrency : r.currency;
  // 1) verify the gateway callback signature (Stripe webhook secret / Alipay sign / WeChat sign)
  // if (!verifyGatewayNotify(req)) return new Response("fail", { status: 400 });
  // 2) create the cashier with the chosen provider, currency = chargeCurrency
  const checkout = await createCheckout(r.provider, order, chargeCurrency);
  return Response.json({ checkoutUrl: checkout.url });
}

API Reference

MethodPathAuthDescription
GET/api/gamesPublicGame list (admin sees all; public only enabled)
POST/api/gamesAdminCreate game
PUT/DELETE/api/games/[slug]AdminUpdate/delete game
POST/api/auth/loginPublicLog into game account, return character list
GET/api/recharge-items?gameSlug=PublicRecharge items for the game (public only enabled)
POST/api/recharge-itemsAdminCreate recharge item
PUT/DELETE/api/recharge-items/[id]AdminUpdate/delete recharge item
POST/api/ordersPublicCreate order (server validates amount)
GET/api/orders/[id]PublicQuery order status
POST/api/orders/[id]/payPublicPayment callback (mock/real)
GET/api/ordersAdminOrder list
POST/api/admin/loginPublicAdmin login, issues session cookie
POST/api/admin/logoutPublicAdmin logout
GET/api/admin/mePublicReturn login state
GET/api/auth/sdk-verify?…Public/api/auth/sdk-verify verification (deep link identity check)

Ops & Deployment

Server Environment (Tencent Cloud)

The app is a single-file SQLite + Node service with very low resource needs. Recommended starting spec (can be lower if traffic is light):

  • Instance: Lightweight Application Server or CVM S5, 2 vCPU / 4 GB RAM minimum
  • Image: TencentOS Server 3.1 / Ubuntu 22.04 LTS
  • Disk: 50 GB SSD system disk; SQLite file is only tens of MB, no data disk needed initially
  • Bandwidth: 1–5 Mbps to start (annual plans are cheaper)
  • Required: Docker Engine 24+ and Docker Compose v2
  • Security group: allow 22 (SSH), 80, 443; app port 3000 internal only, proxied
  • Optional: CLB load balancer, COS for cover images + CDN, SSL cert (Tencent free DV / Let's Encrypt)

⚠️ SQLite is single-file and single-writer, suited for single host / low concurrency. For multi-instance scaling, switch to a cloud DB (see Database & Backup) or writes will lock. Single host is fine initially.

Docker Deployment

# 1. Install Docker (Tencent Cloud mirror one-click, or)
curl -fsSL https://get.docker.com | sh

# 2. Build image
docker build -t game-site .

# 3. Run (persist data to host ./data, limit log size)
docker run -d --name game-site -p 3000:3000 \
  -v $(pwd)/data:/data \
  -e ADMIN_PASSWORD='your strong password' \
  -e AUTH_SECRET='a random long string' \
  --log-driver json-file --log-opt max-size=10m --log-opt max-file=3 \
  game-site

Environment Variables

VariableDescriptionChange?
NEXT_PUBLIC_SITE_NAMESite name, e.g. StarRipple GamesNo
DATABASE_PATHDB path, fixed at /data/app.db in containerNo
ADMIN_PASSWORDAdmin login passwordYes
AUTH_SECRETSession signing secret, a random long stringYes

Reverse Proxy & HTTPS

Use Nginx (or Caddy) to forward 80/443 to container 3000 and obtain SSL:

# /etc/nginx/conf.d/pay.conf
server {
    listen 80;
    server_name pay.your-domain.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
# HTTPS: run certbot --nginx -d pay.your-domain.com to auto-issue and redirect to 443

Caddy is simpler: one line config auto-issues a cert.

Database & Backup

  • SQLite file is at /data/app.db in the container (mapped to ./data/app.db on host)
  • Backup: use sqlite3 .backup for consistency, or cp during off-peak
  • Daily cron example (backup at 3am, keep 7 days)
    0 3 * * * cp /path/data/app.db /path/backup/app.db.$(date +\%F) && find /path/backup -mtime +7 -delete
  • Migrate to cloud DB: replace better-sqlite3 in lib/db.ts with a Postgres driver (e.g. pg); queries are parameterized so cost is low; use TencentDB for PostgreSQL and put the connection string in env

Upgrade & Rollback

  • Upgrade: pull code → docker build -t game-site:new → stop old, run new image. Data in /data volume is unaffected.
  • Rollback: keep the old image tag and run the old version.
  • DB changes: append create/alter in db.migrate() (uses IF NOT EXISTS, idempotent), keep backward compatibility.

Logs & Monitoring

  • View logs: docker logs -f game-site (capped at 10 MB × 3)
  • Basic monitoring: install Tencent Cloud Monitor agent; watch CPU/memory/disk; add APM later
  • Payment callback must be public + HTTPS; configure the notify URL in the merchant console

FAQ

Q:Why does any account log in on demo?

A:It is mocked; after integrating the real backend it validates by login state. See Game Dev integration.

Q:Can it run multi-instance?

A:SQLite has a single-writer limit; single host initially. For multi-instance use TencentDB for PostgreSQL (see Database & Backup).

Q:Is the recharge amount secure?

A:On order creation the server uses the DB price; the front-end price is display-only and cannot be tampered with.

Q:Forgot admin password?

A:Change ADMIN_PASSWORD in .env and restart the container.

Problems? See the section for your role above; cross-team integration follows the Contract fields.