To secure Firebase + Stripe integration, you need to: (1) handle Stripe webhooks in Cloud Functions using rawBody for signature verification, (2) use Firebase Admin SDK to update Firestore subscription fields, (3) protect subscription fields in Security Rules so users cannot modify them, (4) link checkout sessions to Firebase UIDs, and (5) verify authentication in callable functions. This blueprint keeps payment state secure and server-authoritative.
TL;DR
Handle Stripe webhooks in Cloud Functions, not client-side. Verify webhook signatures, use Firebase Admin SDK to update Firestore, and protect subscription status with Security Rules. Never trust client payment confirmations.
Webhook Cloud Function Firebase Stripe
import * as functions from 'firebase-functions'
import Stripe from 'stripe'
import * as admin from 'firebase-admin'
const stripe = new Stripe(functions.config().stripe.secret_key)
const webhookSecret = functions.config().stripe.webhook_secret
export const stripeWebhook = functions.https.onRequest(async (req, res) => {
const signature = req.headers['stripe-signature'] as string
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
signature,
webhookSecret
)
} catch (err) {
console.error('Webhook signature verification failed')
res.status(400).send('Invalid signature')
return
}
const db = admin.firestore()
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
await db.collection('users').doc(session.client_reference_id!).update({
stripeCustomerId: session.customer,
subscriptionStatus: 'active',
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
})
break
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
const userQuery = await db.collection('users')
.where('stripeCustomerId', '==', subscription.customer)
.get()
if (!userQuery.empty) {
await userQuery.docs[0].ref.update({
subscriptionStatus: 'canceled',
})
}
break
}
}
res.json({ received: true })
})
Create Checkout (Callable Function)
import * as functions from 'firebase-functions'
import Stripe from 'stripe'
const stripe = new Stripe(functions.config().stripe.secret_key)
export const createCheckout = functions.https.onCall(async (data, context) => {
// Verify authentication
if (!context.auth) {
throw new functions.https.HttpsError('unauthenticated', 'Must be logged in')
}
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
line_items: [{ price: data.priceId, quantity: 1 }],
success_url: `${data.origin}/success`,
cancel_url: `${data.origin}/cancel`,
client_reference_id: context.auth.uid, // Link to Firebase user
})
return { sessionId: session.id, url: session.url }
})
Firestore Security Rules Firebase
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
// Users can read their own data
allow read: if request.auth.uid == userId;
// Users cannot modify subscription fields
allow update: if request.auth.uid == userId
&& !request.resource.data.diff(resource.data)
.affectedKeys()
.hasAny(['subscriptionStatus', 'stripeCustomerId']);
}
}
}
Protect subscription fields in rules. Users should never be able to modify their own subscription status. Only Cloud Functions with Admin SDK can update these fields.
Security Checklist
Pre-Launch Checklist
Webhook signatures verified
rawBody used for signature verification
Subscription fields protected in rules
Callable functions check auth
Checkout linked to Firebase UID
Related Integration Stacks
Supabase + Stripe Alternative Stripe Webhooks Deep Dive Cloudflare Workers Edge Security