Context and Background
A Rork-built app without a monetization strategy isn't sustainable. Using Stripe, the industry's most trusted payment gateway, you can implement production-grade subscription billing in hours.
Stripe Subscription Fundamentals
Stripe's Three Billing Models
Model Use Case Implementation
One-time Charge Upgrade to paid, add-on features ★☆☆
Subscription Recurring monthly/yearly billing ★★☆
Usage-Based Dynamic pricing per API calls, storage ★★★
This guide focuses on subscriptions .
Three Pricing Patterns
Basic Plan :
Name : "Basic"
Monthly : $9.99
Features : Save 10 articles, ads shown
Stripe Price ID : "price_1MqAaBxxxxxxxxx"
Pro Plan :
Name : "Pro"
Monthly : $19.99
Features : Unlimited saves, ad-free, API access
Stripe Price ID : "price_2NqBbByyyyyyyyy"
Premium Plan :
Name : "Premium"
Monthly : $49.99
Features : Pro + priority support, export
Stripe Price ID : "price_3OqCcCzzzzzzzz"
Step 1: Stripe Account Setup
1.1 Create Plans in Stripe Dashboard
Log into Stripe Dashboard
Navigate to Billing → Products
Click + Create Product
Fill in details:
Product Name: "My App - Pro Plan"
Description: "Unlimited access, ad-free experience"
Type: Service ✓
Recurring: Monthly ✓
After saving, set the monthly price under Pricing tab (e.g., $19.99)
Critical : Save each plan's Price ID . You'll use these in code.
1.2 Register Webhook Endpoint
Webhooks notify your backend when billing events occur (subscription created, renewed, canceled).
In Stripe Dashboard: Developers → Webhooks
Click + Add endpoint
Endpoint URL: https://yourapp.com/api/webhooks/stripe
Events to listen for:
customer.subscription.created
customer.subscription.updated
customer.subscription.deleted
invoice.paid
invoice.payment_failed
Click Create endpoint
Copy the Signing Secret (whsec_...)
Step 2: Frontend Implementation (React/React Native)
2.1 Display Subscription Plans
import { useEffect, useState } from 'react' ;
export function PricingPlans () {
const [ plans , setPlans ] = useState ([]);
useEffect (() => {
// Fetch plans from backend
fetch ( '/api/stripe/plans' )
. then ( res => res. json ())
. then ( data => setPlans (data))
. catch ( err => console. error ( 'Failed to load plans:' , err));
}, []);
const handleSubscribe = async ( priceId ) => {
// Redirect to Checkout
const response = await fetch ( '/api/stripe/checkout' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ priceId })
});
const { sessionId } = await response. json ();
window.location.href = `https://checkout.stripe.com/pay/${ sessionId }` ;
};
return (
< div className = "pricing-grid" >
{ plans. map ( plan => (
< div key = { plan.id } className = "pricing-card" >
< h2 > { plan.name } </ h2 >
< p className = "price" >
$ { plan.price_cents / 100 } / month
</ p >
< ul className = "features" >
{ plan.features. map ( feature => (
< li key = { feature } > { feature } </ li >
)) }
</ ul >
< button
onClick = { () => handleSubscribe (plan.price_id) }
className = "btn-subscribe"
>
Subscribe Now
</ button >
</ div >
)) }
</ div >
);
}
// Expected output:
// ┌─────────────────┬─────────────────┬─────────────────┐
// │ Basic │ Pro │ Premium │
// │ $9.99 / month │ $19.99 / month │ $49.99 / month │
// │ ・10 saves │ ・Unlimited │ ・All Pro │
// │ ・Ads shown │ ・Ad-free │ ・Priority sup. │
// │ [Subscribe] │ [Subscribe] │ [Subscribe] │
// └─────────────────┴─────────────────┴─────────────────┘
2.2 Subscription Status Component
import { useEffect, useState } from 'react' ;
export function UserSubscriptionStatus () {
const [ user , setUser ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
fetch ( '/api/user/subscription-status' )
. then ( res => res. json ())
. then ( data => {
setUser (data);
setLoading ( false );
});
}, []);
if (loading) return < p >Loading...</ p >;
if ( ! user) return < p >Please log in</ p >;
return (
< div className = "user-subscription" >
< h2 >Your Subscription</ h2 >
{ user.subscription ? (
<>
< p >
Plan: < strong > { user.subscription.plan_name } </ strong >
</ p >
< p >
Status: < strong >
{ user.subscription.status === 'active' ? '✓ Active' : '⚠ ' + user.subscription.status }
</ strong >
</ p >
< p >
Next billing: { new Date (user.subscription.next_billing_date). toLocaleDateString () }
</ p >
< button onClick = { () => handleCancelSubscription (user.subscription.id) } >
Cancel Subscription
</ button >
</>
) : (
< p >Free plan. < a href = "/pricing" >Upgrade now</ a ></ p >
) }
</ div >
);
}
const handleCancelSubscription = async ( subscriptionId ) => {
const confirmed = window. confirm ( 'Are you sure? You can re-subscribe anytime.' );
if ( ! confirmed) return ;
await fetch ( '/api/stripe/cancel-subscription' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ subscriptionId })
});
alert ( 'Subscription canceled.' );
window.location. reload ();
};
// Expected output:
// ┌──────────────────────────────────┐
// │ Your Subscription │
// │ Plan: Pro │
// │ Status: ✓ Active │
// │ Next billing: Mar 28, 2026 │
// │ [Cancel Subscription] │
// └──────────────────────────────────┘
Step 3: Backend Implementation (Node.js + Express)
3.1 Initialize Stripe Client
import Stripe from 'stripe' ;
const stripe = new Stripe (process.env. STRIPE_SECRET_KEY , {
apiVersion: '2023-10-16'
});
3.2 Checkout Session Endpoint
// POST /api/stripe/checkout
app. post ( '/api/stripe/checkout' , async ( req , res ) => {
const { priceId } = req.body;
const userId = req.user.id; // From authenticated user
try {
const session = await stripe.checkout.sessions. create ({
payment_method_types: [ 'card' ],
customer_email: req.user.email,
line_items: [
{
price: priceId,
quantity: 1
}
],
mode: 'subscription' ,
success_url: `${ process . env . APP_URL }/success?session_id={CHECKOUT_SESSION_ID}` ,
cancel_url: `${ process . env . APP_URL }/pricing` ,
metadata: {
userId: userId
}
});
res. json ({ sessionId: session.id });
} catch (error) {
console. error ( 'Checkout error:' , error);
res. status ( 500 ). json ({ error: error.message });
}
});
// Expected output:
// POST /api/stripe/checkout
// Request: { "priceId": "price_1MqAaBxxxxxxxxx" }
// Response: { "sessionId": "cs_test_aBcDeFgHiJkLmNoPqRsT..." }
3.3 Webhook Handler (Critical!)
import express from 'express' ;
import bodyParser from 'body-parser' ;
const webhookSecret = process.env. STRIPE_WEBHOOK_SECRET ;
// Webhook endpoint
// ⚠️ Use bodyParser.raw(), NOT JSON (required for Stripe signature verification)
app. post (
'/api/webhooks/stripe' ,
bodyParser. raw ({ type: 'application/json' }),
async ( req , res ) => {
const sig = req.headers[ 'stripe-signature' ];
let event;
try {
event = stripe.webhooks. constructEvent (
req.body,
sig,
webhookSecret
);
} catch (err) {
console. error ( 'Webhook signature verification failed:' , err.message);
return res. sendStatus ( 400 );
}
// Handle events
switch (event.type) {
case 'customer.subscription.created' : {
const subscription = event.data.object;
const userId = subscription.metadata.userId;
await db.subscription. create ({
userId,
stripeSubscriptionId: subscription.id,
stripePriceId: subscription.items.data[ 0 ].price.id,
status: subscription.status,
currentPeriodEnd: new Date (subscription.current_period_end * 1000 )
});
console. log ( `✓ Subscription created for user ${ userId }` );
break ;
}
case 'customer.subscription.updated' : {
const subscription = event.data.object;
const userId = subscription.metadata.userId;
await db.subscription. update (
{ stripeSubscriptionId: subscription.id },
{
status: subscription.status,
currentPeriodEnd: new Date (subscription.current_period_end * 1000 ),
canceledAt: subscription.canceled_at
? new Date (subscription.canceled_at * 1000 )
: null
}
);
console. log ( `✓ Subscription updated for user ${ userId }` );
break ;
}
case 'customer.subscription.deleted' : {
const subscription = event.data.object;
const userId = subscription.metadata.userId;
await db.subscription. update (
{ stripeSubscriptionId: subscription.id },
{ status: 'canceled' , deletedAt: new Date () }
);
await db.user. update (
{ id: userId },
{ tier: 'free' }
);
console. log ( `✓ Subscription deleted for user ${ userId }` );
break ;
}
case 'invoice.paid' : {
const invoice = event.data.object;
console. log ( `✓ Invoice paid: ${ invoice . id }` );
break ;
}
case 'invoice.payment_failed' : {
const invoice = event.data.object;
console. warn ( `⚠ Invoice payment failed: ${ invoice . id }` );
break ;
}
default :
console. log ( `Unhandled event type: ${ event . type }` );
}
res. sendStatus ( 200 );
}
);
// Expected output:
// Webhook received:
// "✓ Subscription created for user 12345"
// "✓ Invoice paid: in_1MqAaBxxxxxxxxx"
// "⚠ Invoice payment failed: in_1MqBbCyyyyyyyyy"
3.4 Subscription Status Endpoint
// GET /api/user/subscription-status
app. get ( '/api/user/subscription-status' , async ( req , res ) => {
const userId = req.user.id;
const subscription = await db.subscription. findFirst ({
where: { userId, status: 'active' }
});
if ( ! subscription) {
return res. json ({
userId,
subscription: null ,
tier: 'free'
});
}
res. json ({
userId,
subscription: {
id: subscription.stripeSubscriptionId,
plan_name: subscription.stripePriceId. includes ( 'pro' ) ? 'Pro' : 'Basic' ,
status: subscription.status,
next_billing_date: subscription.currentPeriodEnd
},
tier: 'premium'
});
});
// Expected output:
// {
// "userId": "12345",
// "subscription": {
// "id": "sub_1MqAaBxxxxxxxxx",
// "plan_name": "Pro",
// "status": "active",
// "next_billing_date": "2026-04-27T10:00:00Z"
// },
// "tier": "premium"
// }
Step 4: Receipt Validation and Security
Client-Side Verification
// On app startup, validate subscription
async function verifySubscriptionOnAppStart () {
const token = localStorage. getItem ( 'stripe_session_token' );
if (token) {
const response = await fetch ( '/api/verify-receipt' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ token })
});
if (response.ok) {
const { isValid , expiresAt } = await response. json ();
if (isValid && new Date () < new Date (expiresAt)) {
activatePremiumFeatures ();
} else {
deactivatePremiumFeatures ();
}
}
}
}
// Expected output:
// App launches
// ↓
// "✓ Subscription valid until: 2026-04-27"
// ↓
// Premium features enabled
Step 5: Churn Reduction Strategies
5.1 Pre-Cancellation Confirmation
function CancelSubscriptionButton ({ subscriptionId }) {
const [ showConfirm , setShowConfirm ] = useState ( false );
const [ selectedReason , setSelectedReason ] = useState ( '' );
const handleCancel = async () => {
// Log cancellation feedback
await fetch ( '/api/subscription/cancel-feedback' , {
method: 'POST' ,
body: JSON . stringify ({
subscriptionId,
cancelReason: selectedReason,
timestamp: new Date ()
})
});
// Process cancellation
await fetch ( '/api/stripe/cancel-subscription' , {
method: 'POST' ,
body: JSON . stringify ({ subscriptionId })
});
setShowConfirm ( false );
alert ( 'Subscription canceled.' );
};
if ( ! showConfirm) {
return (
< button onClick = { () => setShowConfirm ( true ) } className = "btn-danger" >
Cancel Subscription
</ button >
);
}
return (
< div className = "cancel-confirmation" >
< h3 >We're sorry to see you go!</ h3 >
< p >Why are you leaving?</ p >
< select value = { selectedReason } onChange = { ( e ) => setSelectedReason (e.target.value) } >
< option value = "" >-- Select a reason --</ option >
< option value = "too-expensive" >Too expensive</ option >
< option value = "not-using" >Not using enough</ option >
< option value = "found-alternative" >Found alternative</ option >
< option value = "other" >Other</ option >
</ select >
{ selectedReason === 'too-expensive' && (
< div className = "retention-offer" >
< p >💰 50% off for 3 months. Interested?</ p >
< button className = "btn-secondary" >Apply Discount</ button >
</ div >
) }
< button onClick = { handleCancel } className = "btn-danger-confirm" >
Confirm Cancellation
</ button >
< button onClick = { () => setShowConfirm ( false ) } className = "btn-secondary" >
Keep Subscription
</ button >
</ div >
);
}
// Expected output:
// ┌──────────────────────────────────────┐
// │ We're sorry to see you go! │
// │ │
// │ Why are you leaving? │
// │ [v] -- Select reason -- │
// │ → Too expensive │
// │ → Not using enough │
// │ │
// │ [💰 Apply Discount] [Cancel] [Back] │
// └──────────────────────────────────────┘
5.2 Automated Retention Task
// Run monthly
async function monthlyRetentionCheck () {
// Find users scheduled to cancel
const pendingCancellations = await db.subscription. findMany ({
where: {
status: 'active' ,
canceledAt: { not: null }
}
});
for ( const sub of pendingCancellations) {
const daysUntilCancellation = Math. ceil (
( new Date (sub.currentPeriodEnd) - new Date ()) / ( 1000 * 60 * 60 * 24 )
);
// Send winback email 7 days before cancellation
if (daysUntilCancellation === 7 ) {
await sendWinbackEmail ({
email: sub.user.email,
plan: sub.stripePriceId,
offer: 'Special: 50% OFF for 3 months'
});
}
}
}
// Expected output:
// Retention task executed
// Found 23 pending cancellations
// Sent 23 winback emails
A Note from an Indie Developer
Key Takeaways
Stripe transforms a Rork-built app into a sustainable business. Key takeaways:
Webhooks are essential — They're how Stripe tells you about billing events
Validate receipts server-side — Never trust the client alone
Track cancellation reasons — Invaluable for improving retention
Prepare win-back offers — Many customers respond to timely discounts
Want more? Rork Lab publishes advanced Stripe patterns (multi-currency, hybrid one-time + subscription, dunning) regularly. Subscribe for updates.