Wiki · Devlog · Stripe & Firebase

Stripe & Firebase Integration

Monetization for a browser game is a small-but-tricky technical problem. Here is how CONTRABAND handles purchases with Stripe, verification with Firebase, and cross-device sync via Firestore.

The requirements

Players can purchase four paid epilogues ($4.99 each), a bundle ($12.99), and occasional cosmetics ($2.99). Requirements:

The architecture

Four Vercel Functions handle the full payment flow:

Total code across these four functions: approximately 280 lines of Node.js. They share a tiny Stripe/Firebase admin initialization module (~20 lines).

The data model

Firestore holds a single collection: users. Each document keyed by Firebase UID contains:

Guest purchases go into a separate collection guestEntitlements, keyed by a token generated client-side at first visit. When a guest signs up, link-purchase.js moves their entitlements into the authenticated user document.

The client flow

Client purchase flow:

  1. User clicks "Buy Epilogue" in the game UI.
  2. Client calls checkout.js with productId and current auth state (UID if signed in, guest token otherwise).
  3. checkout.js creates Stripe session, returns URL.
  4. Client redirects to Stripe-hosted payment page.
  5. Player enters payment details, submits.
  6. Stripe redirects back to game with success/cancel.
  7. Stripe webhook fires server-side, writes entitlement to Firestore.
  8. Client re-runs verify-purchase.js on return, gets updated entitlements, caches to localStorage.
  9. Epilogue content unlocks in the game.

The round-trip is approximately 8-12 seconds from button click to unlock, most of which is Stripe's own UI. The server-side work is sub-500ms.

Why localStorage as cache

The game cannot always reach Firestore. A player on a bad connection, a plane, or a sandboxed browser environment might fail to sync entitlements. Caching server-confirmed entitlements to localStorage means the game runs correctly offline as long as the player has ever successfully synced.

The security model is: localStorage is trusted by the client for reading (display UI), but any write must be server-confirmed. A malicious user who modifies localStorage gets a falsely-unlocked local UI, but no content renders because the actual epilogue content is loaded from a server endpoint that checks Firestore before serving. The localStorage cache is a UX optimization, not an authority.

Common pitfalls

What I learned

Stripe + Firebase + Vercel is genuinely the shortest path to a working monetization system for a browser game. The total infrastructure cost is <$10/month at any reasonable scale. The code is short enough that one developer can maintain it.

The key architectural insight is the cache-with-authoritative-source pattern. Firestore is the source of truth; localStorage is cache; the client trusts itself for UI but the server for actual content access. This pattern scales from one paying player to tens of thousands without fundamentally changing.