FAHSWE

Developer documentation

Securely integrate Fah Swe H5 games into Android, iOS, Flutter and web apps

This public guide describes the exact production architecture. Game files remain on Fah Swe's global CDN, private credentials remain on your backend, and the app receives only a short-lived launch URL.

Security model

  • Create signed catalog and session requests only on your trusted backend. Never embed the App Secret in APK, IPA, Flutter assets or browser JavaScript.
  • The server validates active subscription, enabled game, requested mode, timestamp, nonce and HMAC signature before issuing a session.
  • Launch URLs are short-lived. Do not log, cache, persist or share them.
  • Treat WebView messages as untrusted input; validate the exact CDN origin and allow-list event types.

Production launch flow

  1. 1Load the dynamic game catalog through your own authenticated backend.
  2. 2When a user taps a game, your backend signs POST /v1/sessions with X-Fah-* headers.
  3. 3Your backend returns only launchUrl and expiry to the signed-in app.
  4. 4Open launchUrl in full-screen or in the lower voice-room panel. Use the per-game view metadata instead of hard-coded heights.
  5. 5The game exchanges the one-time token and checks subscription state on the server throughout play.

1. Sign the session request on your backend

Node.js / TypeScript
// server/fahswe-session.ts — trusted backend only
import crypto from "node:crypto";

const API = "https://game-api.fahswe.com/v1";
const APP_ID = process.env.FAHSWE_APP_ID!;
const APP_SECRET = process.env.FAHSWE_APP_SECRET!;

function sha256(value: string) {
  return crypto.createHash("sha256").update(value).digest("hex");
}

export async function createGameSession(input: {
  externalUserId: string;
  gameSlug: string;
  mode: "full-screen" | "voice-room";
  roomId?: string;
  language?: "en" | "bn";
}) {
  const path = "/v1/sessions";
  const timestamp = String(Date.now());
  const nonce = crypto.randomBytes(24).toString("base64url");
  const body = JSON.stringify({
    ...input,
    language: input.language ?? "en",
    safeArea: { top: 0, bottom: 0, standard: 1 },
  });
  const canonical = ["POST", path, timestamp, nonce, sha256(body)].join("\n");
  const signature = crypto
    .createHmac("sha256", APP_SECRET)
    .update(canonical)
    .digest("hex");

  const response = await fetch(API + "/sessions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Fah-App-Id": APP_ID,
      "X-Fah-Timestamp": timestamp,
      "X-Fah-Nonce": nonce,
      "X-Fah-Signature": signature,
    },
    body,
    cache: "no-store",
  });
  const data = await response.json();
  if (!response.ok) throw new Error(data.error ?? "SESSION_FAILED");
  return data; // Return only data.launchUrl to the mobile app.
}

2. Expose a protected endpoint to your own app

Your authenticated API
// POST /api/games/session — your app's authenticated API
const user = await requireSignedInUser(request);
const { gameSlug, mode, roomId } = await request.json();

const session = await createGameSession({
  externalUserId: user.id,       // stable ID from your database
  gameSlug,                      // from the signed catalog
  mode,                          // full-screen | voice-room
  roomId: mode === "voice-room" ? roomId : undefined,
  language: user.language,
});

return Response.json({
  launchUrl: session.launchUrl,  // short-lived; do not store it
  expiresAt: session.expiresAt,
  view: session.view,
});

3. Open the launch URL in the client

Flutter
final response = await api.post('/api/games/session', data: {
  'gameSlug': game.id,
  'mode': isVoiceRoom ? 'voice-room' : 'full-screen',
  if (isVoiceRoom) 'roomId': roomId,
});

final launchUrl = response.data['launchUrl'] as String;
final controller = WebViewController()
  ..setJavaScriptMode(JavaScriptMode.unrestricted)
  ..setNavigationDelegate(NavigationDelegate(
    onNavigationRequest: (request) {
      final uri = Uri.parse(request.url);
      return uri.scheme == 'https' && uri.host == 'games.fahswe.com'
          ? NavigationDecision.navigate
          : NavigationDecision.prevent;
    },
  ))
  ..loadRequest(Uri.parse(launchUrl));
Android Kotlin
// launchUrl comes from YOUR authenticated backend, never from the APK.
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.allowFileAccess = false
webView.settings.allowContentAccess = false
webView.settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW
webView.webViewClient = object : WebViewClient() {
  override fun shouldOverrideUrlLoading(v: WebView, r: WebResourceRequest): Boolean {
    val u = r.url
    return u.scheme != "https" || u.host != "games.fahswe.com"
  }
}
webView.loadUrl(launchUrl)
iOS Swift
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .default()
let webView = WKWebView(frame: .zero, configuration: configuration)

guard launchURL.scheme == "https",
      launchURL.host == "games.fahswe.com" else { return }
webView.load(URLRequest(url: launchURL,
  cachePolicy: .reloadIgnoringLocalCacheData))
React Native
<WebView
  source={{ uri: launchUrl }}
  javaScriptEnabled
  domStorageEnabled
  originWhitelist={["https://games.fahswe.com"]}
  onShouldStartLoadWithRequest={({ url }) =>
    new URL(url).origin === "https://games.fahswe.com"
  }
  onMessage={({ nativeEvent }) => handleAllowListedEvent(nativeEvent.data)}
/>
Web iframe
<iframe
  src={launchUrl}
  title="Fah Swe game"
  allow="autoplay; fullscreen"
  sandbox="allow-scripts allow-same-origin"
  referrerPolicy="no-referrer"
/>

window.addEventListener("message", (event) => {
  if (event.origin !== "https://games.fahswe.com") return;
  if (event.data?.source !== "fahswe-game") return;
  // Allow-list: FAHSWE_GAME_LOADED, FAHSWE_GAME_CLOSE,
  // FAHSWE_GAME_RECHARGE, FAHSWE_ROUND_RESULT, FAHSWE_GAME_BLOCKED.
});

Full-screen and voice-room modes

Send mode=full-screen for a dedicated game page. Send mode=voice-room with a non-empty roomId for an embedded lower panel. Read voiceHeightPercent and minVoiceHeightDp from each catalog item so Teen Patti and future games scale correctly without app updates.

Server-authoritative individual and shared rounds

Each catalog game declares its round mode. INDIVIDUAL games such as Fortune Game start instantly for the player who presses Spin and resolve one secure server outcome for that session. SHARED games synchronize one Fah Swe round ID, UTC-aligned deadlines and a common outcome across players. In both modes, never add a host-side random result, retry result or balance calculation; repeated settlement is idempotent.

Wallet timing and atomic settlement

During betting, the H5 game immediately displays available balance as authoritative balance minus the current round's reserved bets. Do not debit the client wallet for each chip tap. When the secure round resolves, Fah Swe sends one signed result settlement containing stakeMinor, payoutMinor and netMinor = payoutMinor - stakeMinor. Verify the callback, atomically apply netMinor once, and return the updated integer balance. Replays must return the original stored result without changing the balance again.

Server settlement contract
// One signed callback is sent when a secure round resolves.
// Chip taps reserve balance in the H5 display; they are not backend debits.
const { orderId, idempotencyKey, stakeMinor, payoutMinor, netMinor } = body;

if (netMinor !== payoutMinor - stakeMinor) {
  return Response.json({ error: "SETTLEMENT_INVALID" }, { status: 400 });
}

// After verifying the HMAC, timestamp and nonce:
// 1. Atomically apply netMinor exactly once for orderId/idempotencyKey.
// 2. On replay, return the stored result without applying it again.
// 3. Return the authoritative integer balance after settlement.
return Response.json({
  status: true,
  data: { balanceMinor: updatedBalance },
});

Host bridge events

The shared SDK posts versioned messages with source=fahswe-game. Allow only documented FAHSWE_* types, verify event.origin is exactly https://games.fahswe.com, and refresh wallet state from your backend. Never trust a client-supplied balance or result.

Subscription behavior

Only active subscribers receive sessions. A payment-due grace state shows a warning. After grace expires the server blocks new sessions. Once an invoice is verified, backend entitlement becomes active again without changing the mobile app.

Virtual entertainment only

Fah Swe does not provide gambling, cash wagering, cash redemption or transferable-value credits. Integrators remain responsible for age controls, local law, app-store rules and truthful disclosures.