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.
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 });
}In-game Deep Link Recharge
When a player taps "Recharge" inside the game (via the account SDK), the game client builds a signed Deep Link that opens the official recharge page with the account and character info pre-filled — skipping manual login and going straight to "Select Package". This guide is for game client and SDK integrators.
1. Scenario Matrix
There is one main "official recharge" flow, with three branches depending on the entry point:
- PC browser opening the official site: enter from the homepage, full four steps (select game → login → select package → pay). For desktop users.
- Mobile browser opening the official site: mobile-first responsive layout, same flow as PC.
- Mobile in-game WebView jump: tap recharge → SDK builds signed Deep Link → opens official recharge page (inside WebView) → account/character auto-filled → straight to "Select Package" → pay → tap "Back to game" to return.
2. Link Spec & Parameters
The Deep Link base path is /recharge, and context is carried via query params. The normal entry only needs `game`; the in-game jump needs the full signed params.
| Param | Required | Description | Example |
|---|---|---|---|
| game | ✅ | Target game slug (same as /api/games) | duomi-planet |
| aid | ✅ | Account ID (unique id after SDK login) | u_12345 |
| name | ✅ | Account display name | PlayerOne |
| cid | ✅ | Character ID | c_88 |
| cname | ✅ | Character name | SwordSaint |
| sid | ✅ | Character server | S1 |
| lv | ✅ | Character level | 60 |
| token | ✅ | Session token (SDK login state, used for server introspection) | abc…def |
| ts | ✅ | Signature timestamp (Unix seconds), valid for 5 minutes by default | 1700000000 |
| sig | ✅ | HMAC-SHA256 signature | hmac… |
| lang | — | UI language zh/en/vi/th (optional, overrides auto-detect) | vi |
| ret | — | Return-to-game URL after payment, e.g. duomi://recharge/done (optional) | duomi://recharge/done |
/recharge?game=duomi-planet&aid=u_12345&name=PlayerOne&cid=c_88&cname=SwordSaint&sid=S1&lv=60&token=abc...&ts=1700000000&sig=hmac&ret=duomi://recharge/done3. Signing Algorithm
The server endpoint /api/auth/sdk-verify checks in order: ① all required params present; ② ts not expired (±5 min); ③ HMAC-SHA256 over all params except sig, sorted by key, compared with the provided sig; ④ token introspection (demo: non-empty passes; production should validate the SDK session).
// Client and server use the same rules
const params = { game, aid, name, cid, cname, sid, lv, token, ts };
const payload = Object.keys(params).sort()
.map(k => k + "=" + params[k]).join("&");
const sig = HMAC_SHA256(payload, SDK_LINK_SECRET); // secret stays server-side only4. Client Link Generation
Reference implementation for the game client to build the Deep Link (keep the secret on the game server, never hardcode it in the client bundle):
// Game client (secret lives on the game server, never in the client bundle)
async function buildRechargeLink(account, character) {
const ts = Math.floor(Date.now() / 1000);
const params = {
game: account.gameSlug, aid: account.id, name: account.name,
cid: character.id, cname: character.name,
sid: character.server, lv: String(character.level),
token: account.sessionToken, ts: String(ts),
ret: "duomi://recharge/done",
};
const payload = Object.keys(params).sort()
.map(k => k + "=" + params[k]).join("&");
const sig = await hmacSha256(payload, SECRET); // call the game server's signing endpoint
const q = new URLSearchParams({ ...params, sig }).toString();
return "https://pay.your-domain.com/recharge?" + q;
}5. Redirect Sequence
Sequence: tap recharge in-game → SDK fetches session → client computes signature → open official /recharge?… → frontend POSTs /api/auth/sdk-verify → on success renders "Select Package" → create /api/orders → pay → after done, jump back to game via ret.
Tap "Recharge" inside the game
|
+- SDK fetches session (account/character/token)
|
+- Client computes HMAC signature per the rules
|
+- Opens official site /recharge?game=...&sig=... (inside WebView)
|
+- Frontend POST /api/auth/sdk-verify
| +- verifies signature/expiry/token
| +- returns verified account + character
|
+- Renders "Select recharge" (skips select-game / login)
|
+- Place order POST /api/orders -> pay -> success
|
+- Tap "Return to game" -> jumps back to game via ret6. Return to Game
After payment, if the Deep Link carries `ret` and it is a safe scheme (http(s) or a custom scheme like duomi://), the page shows a "Back to game" button; tapping it invokes the game with that URL. The server guards `ret` against open-redirect (blocks javascript:/data: etc.).
7. Responsive & WebView
The site is mobile-first throughout: game cards and packages stack on phones and show two columns on PC; the stepper hides text on narrow screens and keeps only the numbers; inside a WebView it auto-switches zh/en/vi/th by system language or the Deep Link `lang` param.
8. Security Notes
Security notes: ① the signing secret SDK_LINK_SECRET lives only on the server, never in the frontend; ② links expire after 5 minutes + ts prevents replay; ③ in production, token should introspect the SDK session to confirm the user is logged in; ④ ret is allow-listed by scheme; ⑤ both normal and Deep Link entries share the same order/pay APIs, and amounts are read server-side from the DB so the frontend cannot tamper with them.
⚠️ 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.
API Reference
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/games | Public | Game list (admin sees all; public only enabled) |
| POST | /api/games | Admin | Create game |
| PUT/DELETE | /api/games/[slug] | Admin | Update/delete game |
| POST | /api/auth/login | Public | Log into game account, return character list |
| GET | /api/recharge-items?gameSlug= | Public | Recharge items for the game (public only enabled) |
| POST | /api/recharge-items | Admin | Create recharge item |
| PUT/DELETE | /api/recharge-items/[id] | Admin | Update/delete recharge item |
| POST | /api/orders | Public | Create order (server validates amount) |
| GET | /api/orders/[id] | Public | Query order status |
| POST | /api/orders/[id]/pay | Public | Payment callback (mock/real) |
| GET | /api/orders | Admin | Order list |
| POST | /api/admin/login | Public | Admin login, issues session cookie |
| POST | /api/admin/logout | Public | Admin logout |
| GET | /api/admin/me | Public | Return 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-siteEnvironment Variables
| Variable | Description | Change? |
|---|---|---|
| NEXT_PUBLIC_SITE_NAME | Site name, e.g. StarRipple Games | No |
| DATABASE_PATH | DB path, fixed at /data/app.db in container | No |
| ADMIN_PASSWORD | Admin login password | Yes |
| AUTH_SECRET | Session signing secret, a random long string | Yes |
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 443Caddy 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.