Overview
Deltalytix uses Stripe for secure payment processing and subscription management. This integration handles both recurring subscriptions and one-time lifetime purchases.
Configuration
Environment Variables
Add the following variables to your .env file:
# Stripe Configuration
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = 'pk_test_...'
STRIPE_SECRET_KEY = 'sk_test_...'
STRIPE_WEBHOOK_SECRET = 'whsec_...'
Never commit your Stripe secret keys to version control. Always use environment variables.
Stripe Client Initialization
The Stripe client is initialized in server/stripe.ts:
import Stripe from "stripe" ;
export const stripe = new Stripe ( process . env . STRIPE_SECRET_KEY as string , {
apiVersion: "2025-10-29.clover" ,
});
Subscription Management
Subscription Types
Deltalytix supports two subscription models:
Recurring Subscriptions - Monthly, quarterly, or yearly billing
Lifetime Plans - One-time payment with permanent access
Creating Checkout Sessions
Checkout sessions are created in app/api/stripe/create-checkout-session/route.ts:
// Fetch the price by lookup key
const prices = await stripe . prices . list ({
lookup_keys: [ lookup_key ],
expand: [ 'data.product' ],
});
const price = prices . data [ 0 ];
const isLifetimePlan = price . type === 'one_time' ;
// Configure session based on plan type
const sessionConfig : any = {
customer: customerId ,
metadata: {
plan: lookup_key ,
... ( referral && { referral_code: referral }),
},
line_items: [
{
price: price . id ,
quantity: 1 ,
},
],
success_url: ` ${ websiteURL } dashboard?success=true` ,
cancel_url: ` ${ websiteURL } pricing?canceled=true` ,
allow_promotion_codes: true ,
};
if ( isLifetimePlan ) {
sessionConfig . mode = 'payment' ;
} else {
sessionConfig . mode = 'subscription' ;
}
const session = await stripe . checkout . sessions . create ( sessionConfig );
The system automatically detects if a customer already exists and reuses their customer ID to maintain payment history.
Retrieving Subscription Data
The getSubscriptionData() function in server/billing.ts handles subscription retrieval with this priority:
Local database lifetime subscriptions (highest priority)
Active Stripe subscriptions
Trialing Stripe subscriptions
export async function getSubscriptionData () {
const supabase = await createClient ();
const { data : { user } } = await supabase . auth . getUser ();
if ( ! user ?. email ) throw new Error ( 'User not found' );
// Check local database for lifetime subscription
const localSubscription = await prisma . subscription . findUnique ({
where: { email: user . email },
});
if ( localSubscription &&
localSubscription . status === 'ACTIVE' &&
localSubscription . interval === 'lifetime' ) {
return createLifetimeSubscriptionData ( localSubscription , invoices . data );
}
// Check Stripe for active subscriptions
const customers = await stripe . customers . list ({
email: user . email ,
limit: 1 ,
});
const subscriptions = await stripe . subscriptions . list ({
customer: customer . id ,
status: 'active' ,
expand: [ 'data.plan' , 'data.items.data.price' , 'data.discounts.coupon' ],
limit: 1 ,
});
// Return subscription data with invoices
return {
id: subscription . id ,
status: subscription . status ,
current_period_end: getCurrentPeriodEnd ( subscription ),
plan: {
id: price . id ,
name: subscriptionPlan ,
amount: price . unit_amount || 0 ,
interval: price . recurring ?. interval || 'month' ,
},
invoices: invoices . data ,
};
}
Updating Subscriptions
Users can pause, resume, or cancel their subscriptions:
export async function updateSubscription (
action : 'pause' | 'resume' | 'cancel' ,
subscriptionId : string
) {
if ( action === 'pause' || action === 'cancel' ) {
await stripe . subscriptions . update ( subscriptionId , {
cancel_at_period_end: true
});
} else if ( action === 'resume' ) {
await stripe . subscriptions . update ( subscriptionId , {
cancel_at_period_end: false
});
}
return { success: true };
}
Switching Plans
The switchSubscriptionPlan() function handles plan upgrades and downgrades:
export async function switchSubscriptionPlan ( newLookupKey : string ) {
// Get new price
const prices = await stripe . prices . list ({
lookup_keys: [ newLookupKey ],
expand: [ 'data.product' ],
});
const newPrice = prices . data [ 0 ];
// Update subscription with prorating
const updatedSubscription = await stripe . subscriptions . update (
currentSubscription . id ,
{
items: [{
id: currentSubscriptionItem . id ,
price: newPrice . id ,
}],
proration_behavior: 'create_prorations' ,
}
);
return { success: true , subscription: updatedSubscription };
}
Webhook Handling
Webhook Endpoint
Webhooks are handled in app/api/stripe/webhooks/route.ts. This endpoint receives real-time events from Stripe.
Webhook Signature Verification
Always verify webhook signatures to ensure requests come from Stripe:
export async function POST ( req : Request ) {
let event : Stripe . Event ;
try {
event = stripe . webhooks . constructEvent (
await ( await req . blob ()). text (),
req . headers . get ( "stripe-signature" ) as string ,
process . env . STRIPE_WEBHOOK_SECRET as string ,
);
} catch ( err ) {
return NextResponse . json (
{ message: `Webhook Error: ${ err } ` },
{ status: 400 },
);
}
// Process event
console . log ( "✅ Success:" , event . id );
}
Never process webhook events without signature verification. This protects against malicious requests.
Supported Events
Deltalytix handles the following Stripe events:
const permittedEvents : string [] = [
"checkout.session.completed" ,
"payment_intent.succeeded" ,
"payment_intent.payment_failed" ,
"customer.subscription.deleted" ,
"customer.subscription.updated" ,
"customer.subscription.created" ,
"invoice.payment_failed" ,
"customer.subscription.trial_will_end"
];
Event Handler Examples
Checkout Session Completed
case "checkout.session.completed" :
data = event . data . object as Stripe . Checkout . Session ;
if ( data . mode === 'subscription' && data . subscription ) {
// Handle recurring subscription
const subscription = await stripe . subscriptions . retrieve (
data . subscription as string
);
const priceId = subscriptionItems . data [ 0 ]?. price . id ;
const price = await stripe . prices . retrieve ( priceId , {
expand: [ 'product' ],
});
const productName = ( price . product as Stripe . Product ). name ;
const subscriptionPlan = productName . toUpperCase ();
const interval = price . recurring ?. interval_count === 3
? 'quarter'
: price . recurring ?. interval || 'month' ;
await prisma . subscription . upsert ({
where: { email: data . customer_details ?. email as string },
update: {
plan: subscriptionPlan ,
endDate: new Date ( currentPeriodEnd * 1000 ),
status: 'ACTIVE' ,
interval: interval ,
},
create: {
email: data . customer_details ?. email as string ,
plan: subscriptionPlan ,
endDate: new Date ( currentPeriodEnd * 1000 ),
status: 'ACTIVE' ,
interval: interval ,
}
});
} else if ( data . mode === 'payment' ) {
// Handle one-time payment (lifetime plan)
const lifetimeEndDate = new Date ();
lifetimeEndDate . setFullYear ( lifetimeEndDate . getFullYear () + 100 );
await prisma . subscription . upsert ({
where: { email: data . customer_details ?. email as string },
update: {
plan: subscriptionPlan ,
endDate: lifetimeEndDate ,
status: 'ACTIVE' ,
interval: 'lifetime' ,
},
create: {
email: data . customer_details ?. email as string ,
plan: subscriptionPlan ,
endDate: lifetimeEndDate ,
status: 'ACTIVE' ,
interval: 'lifetime' ,
}
});
}
break ;
Subscription Updated
case "customer.subscription.updated" :
data = event . data . object as Stripe . Subscription ;
const updatedCustomerData = await stripe . customers . retrieve (
data . customer as string
) as Stripe . Customer ;
if ( data . cancel_at_period_end ) {
// Subscription scheduled for cancellation
await prisma . subscription . update ({
where: { email: updatedCustomerData . email },
data: {
status: "SCHEDULED_CANCELLATION" ,
endDate: new Date ( currentPeriodEnd * 1000 )
}
});
// Save cancellation feedback if provided
const cancellationDetails =
data . cancellation_details as Stripe . Subscription . CancellationDetails ;
if ( cancellationDetails ?. feedback || cancellationDetails ?. comment ) {
await prisma . subscriptionFeedback . create ({
data: {
email: updatedCustomerData . email ,
event: "SCHEDULED_CANCELLATION" ,
cancellationReason: cancellationDetails . feedback || null ,
feedback: cancellationDetails . comment || null
}
});
}
}
break ;
Subscription Deleted
case "customer.subscription.deleted" :
data = event . data . object as Stripe . Subscription ;
const customerData = await stripe . customers . retrieve (
data . customer as string
) as Stripe . Customer ;
if ( customerData . email ) {
await prisma . subscription . update ({
where: { email: customerData . email },
data: {
plan: 'FREE' ,
status: "CANCELLED" ,
endDate: new Date ( data . ended_at ! * 1000 )
}
});
}
break ;
Payment Failed
case "invoice.payment_failed" :
data = event . data . object as Stripe . Invoice ;
const customerEmail = ( await stripe . customers . retrieve (
data . customer as string
) as Stripe . Customer ). email ;
if ( customerEmail ) {
await prisma . subscription . update ({
where: { email: customerEmail },
data: { status: "PAYMENT_FAILED" }
});
}
break ;
Security Best Practices
API Key Security
Use Environment Variables
Store all Stripe keys in environment variables, never in code: STRIPE_SECRET_KEY = 'sk_live_...'
STRIPE_WEBHOOK_SECRET = 'whsec_...'
Separate Test and Production Keys
Use different keys for development and production: # Development
STRIPE_SECRET_KEY = 'sk_test_...'
# Production
STRIPE_SECRET_KEY = 'sk_live_...'
Restrict API Key Permissions
Create restricted API keys in the Stripe Dashboard with only the permissions you need.
Webhook Security
Always verify webhook signatures using stripe.webhooks.constructEvent()
Use HTTPS for your webhook endpoint in production
Implement idempotency to handle duplicate events
Log all webhook events for debugging and audit trails
PCI Compliance
Deltalytix never handles raw card data. Stripe Checkout handles all payment information, keeping your application PCI compliant.
Testing
Test Mode
Use Stripe test mode during development:
# Test keys start with sk_test_ and pk_test_
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = 'pk_test_...'
STRIPE_SECRET_KEY = 'sk_test_...'
Webhook Testing
Test webhooks locally using the Stripe CLI:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login to your Stripe account
stripe login
# Forward webhooks to your local server
stripe listen --forward-to localhost:3000/api/stripe/webhooks
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
Test Cards
Use Stripe’s test card numbers:
Success : 4242 4242 4242 4242
Requires authentication : 4000 0025 0000 3155
Declined : 4000 0000 0000 0002
Common Issues
Webhook signature verification fails
Ensure STRIPE_WEBHOOK_SECRET matches the webhook endpoint secret in Stripe Dashboard
Check that you’re using raw body, not parsed JSON
Verify the webhook endpoint URL is correctly configured in Stripe
Customer not found after checkout
Ensure webhook handler is processing checkout.session.completed events
Check database connection in webhook handler
Verify email is being correctly extracted from checkout session
Subscription shows as inactive
Check subscription status in Stripe Dashboard
Verify customer.subscription.updated webhook is being received
Look for payment failures or expired cards
Additional Resources
Stripe API Documentation Official Stripe API reference
Stripe Webhooks Guide Complete guide to webhook implementation
Stripe Testing Test cards and testing strategies
Stripe Dashboard Manage customers and subscriptions