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:
- Server-side verification. Client-side "purchased" flags are easily spoofed. The server must authoritatively know who has bought what.
- Cross-device sync. Buy an epilogue on your phone, unlock on your laptop at next sign-in.
- Offline tolerance. The game should run without a server connection except at purchase moments and sync-in moments.
- Guest purchases. Players should be able to buy without creating an account, then link later when they sign up.
The architecture
Four Vercel Functions handle the full payment flow:
- checkout.js — Creates a Stripe Checkout session with the product catalog. Returns session URL to client, which redirects the browser.
- stripe-webhook.js — Listens for Stripe events (checkout.session.completed). On successful payment, writes entitlements to Firestore under the user's UID (or guest token if unauthenticated).
- verify-purchase.js — Called by the client on game load. Returns the user's current entitlements from Firestore. Client caches result to localStorage.
- link-purchase.js — Called when a guest purchaser later creates an account. Links guest-token purchases to the new UID.
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:
entitlements— Array of product IDs the user has purchased.guestTokens— Array of guest tokens linked to this account (used for purchase attribution before sign-up).purchaseHistory— Array of {productId, timestamp, stripeSessionId} for audit.
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:
- User clicks "Buy Epilogue" in the game UI.
- Client calls
checkout.jswith productId and current auth state (UID if signed in, guest token otherwise). - checkout.js creates Stripe session, returns URL.
- Client redirects to Stripe-hosted payment page.
- Player enters payment details, submits.
- Stripe redirects back to game with success/cancel.
- Stripe webhook fires server-side, writes entitlement to Firestore.
- Client re-runs verify-purchase.js on return, gets updated entitlements, caches to localStorage.
- 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
- Webhook race conditions. If a user returns from Stripe before the webhook fires, verify-purchase.js returns stale data. Fix: client polls verify-purchase every 2 seconds for 30 seconds after Stripe return until the new entitlement shows up.
- Guest token loss. If a player buys as guest then clears localStorage, their token is gone and the purchase is orphaned. Fix: email receipt from Stripe contains a "recovery link" that verifies against Stripe session ID and re-attaches purchase to a new token.
- Webhook signing secrets. Stripe webhooks must verify signatures. Failing to do so allows anyone to POST fake purchases. This is the #1 mistake in Stripe integrations and I made it once.
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.