← knowledge.oriz.in

Billing webhook architecture: CF Pages Function → Firestore

decision decisionbillingwebhookrazorpaypaddleplay-billingms-storecf-pages-function

Billing webhook architecture

Decision

4 webhook endpoints (one per payment provider), each implemented as a Cloudflare Pages Function:

Provider Webhook URL Auth check
Razorpay (INR) oriz.in/api/billing-webhook/razorpay HMAC-SHA256 via X-Razorpay-Signature header + shared secret
Paddle (ROW) oriz.in/api/billing-webhook/paddle HMAC-SHA256 via Paddle-Signature header + shared secret
Play Billing (Android) oriz.in/api/billing-webhook/play Service-account JWT verification via Authorization: Bearer
Microsoft Store (Windows) oriz.in/api/billing-webhook/ms-store OAuth2 service principal

Why CF Pages Functions (not Workers)

Pages Function is the simplest no-card option. 1 function call per purchase ? far below quota.

Webhook payload contract

Each handler normalizes the provider's payload to a single internal shape before writing to Firestore:

type SubscriptionUpdate = {
  userId: string;          // looked up from provider's customer.email
  tier: 'ad-free' | 'pro';
  status: 'active' | 'expired' | 'cancelled';
  expiresAt: number;       // unix ms
  source: 'razorpay' | 'paddle' | 'play' | 'ms-store';
  externalRef: string;     // provider's subscription/order ID
};

// Write path:
firestore.doc(`users/${userId}/subscriptions/${source}`).set(update)

User lookup: provider passes customer email at checkout. Pages Function queries Firestore users collection by email to find uid. If user doesn't exist yet (first purchase), the Pages Function creates a pending users/{tempId} doc; account-link happens on first login via Firebase Auth.

Firestore reads in apps

Each app's BaseLayout pulls users/{uid}/subscriptions/* on auth state change (Firestore client SDK, real-time listener). On change:

Cross-app SSO via Firebase Auth means a single subscription unlocks all apps.

Pricing page architecture

Per single-pricing-page-package.md, the /pricing route is shipped from @chirag127/astro-billing and mounted on every app. Buttons on the page are direct links to the provider's hosted checkout (Razorpay Payment Page, Paddle Checkout Page, Play Billing flow, MS Store IAP).

On purchase: provider's hosted checkout completes ? posts webhook ? CF Pages Function writes Firestore ? apps see the change live via Firestore listener.

Webhook secrets

Stored at chirag127 GH org level (per github-org-level-secrets.md):

Added to templates/.env.example as DOCUMENTED env vars (with comments explaining each provider's setup URL).

Cross-refs