The boilerplate ships with checkout, webhooks, and logging ready inside src/app/api/stripe. This page walks through connecting your account and validating the payment flow.
Stripe is the default payment processor for ignitionstack.pro, supporting one-time payments, subscriptions, and checkout sessions.
src/app/
├── api/stripe/
│ ├── checkout/route.ts # Create checkout session
│ ├── webhooks/route.ts # Receive Stripe events
│ └── portal/route.ts # Customer portal (subscriptions)
├── actions/stripe/
│ ├── create-checkout.ts # Checkout server action
│ └── manage-subscription.ts # Manage subscriptions
└── server/stripe/
├── get-customer.ts # Fetch customer
└── get-subscription.ts # Fetch subscription statusGo to dashboard.stripe.com and create an account. Keep Test Mode enabled for development.
No Dashboard → Developers → API Keys:
# Stripe Keys
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# Webhook Secret (configure em Webhooks)
STRIPE_WEBHOOK_SECRET=whsec_...price_id for checkout// Usage example
const priceId = 'price_1234567890'For local development use the Stripe CLI:
# Install Stripe CLI
brew install stripe/stripe-cli/stripe
# Login
stripe login
# Listen for events (separate terminal)
stripe listen --forward-to localhost:3000/api/stripe/webhooksThe CLI prints the webhook secret (whsec_...) you must copy into .env.local.
'use server'
import { stripe } from '@/lib/stripe'
import { createServerClient } from '@/lib/supabase/server'
export async function createCheckoutSession(priceId: string) {
const supabase = await createServerClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return { error: 'Unauthorized' }
}
const session = await stripe.checkout.sessions.create({
customer_email: user.email,
line_items: [{ price: priceId, quantity: 1 }],
mode: 'payment', // or 'subscription'
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
metadata: {
userId: user.id,
},
})
return { url: session.url }
}'use client'
import { useState } from 'react'
import { createCheckoutSession } from '@/app/actions/stripe/create-checkout'
export function CheckoutButton({ priceId }: { priceId: string }) {
const [loading, setLoading] = useState(false)
async function handleCheckout() {
setLoading(true)
const { url, error } = await createCheckoutSession(priceId)
if (url) {
window.location.href = url
} else {
console.error(error)
setLoading(false)
}
}
return (
<button onClick={handleCheckout} disabled={loading}>
{loading ? 'Processing...' : 'Buy Now'}
</button>
)
}import { headers } from 'next/headers'
import { stripe } from '@/lib/stripe'
import { createAdminClient } from '@/lib/supabase/admin'
import type Stripe from 'stripe'
export async function POST(req: Request) {
const body = await req.text()
const signature = headers().get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return new Response('Webhook signature verification failed', { status: 400 })
}
const supabase = createAdminClient()
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
// Update user in Supabase
await supabase
.from('users')
.update({
stripe_customer_id: session.customer,
has_purchased: true
})
.eq('id', session.metadata?.userId)
break
}
case 'customer.subscription.updated':
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription
await supabase
.from('subscriptions')
.upsert({
stripe_subscription_id: subscription.id,
status: subscription.status,
current_period_end: new Date(subscription.current_period_end * 1000),
})
break
}
}
return new Response('OK', { status: 200 })
}Affiliates get their own coupon + promotion code automatically during signup:
src/app/actions/affiliates/become-affiliate.ts creates a Stripe coupon (AFFILIATE_<CODE>) with the affiliate discount rate.stripe_coupon_id and stripe_promotion_code_id) are stored on the affiliates table for future reference.Stripe Dashboard → Marketing → Coupons / Promotion codes will show the entries created by the boilerplate. If you need to reset one manually, delete the promotion code and coupon there, then re-run the becomeAffiliate action (or update via Stripe API) so the Supabase row stays in sync.
⚠️ Remember to re-run the latest migrations (
supabase/migrations/schema/73_affiliate_promotion_codes.sql) so thestripe_promotion_code_idcolumn exists before deploying.
Always test with Stripe’s test cards before going live!
| Scenario | Card number |
|---|---|
| Success | 4242 4242 4242 4242 |
| Requires authentication | 4000 0025 0000 3155 |
| Payment declined | 4000 0000 0000 9995 |
checkout.session.completedFor subscriptions, enable the Customer Portal inside the Stripe Dashboard and implement:
import { stripe } from '@/lib/stripe'
import { createServerClient } from '@/lib/supabase/server'
export async function POST() {
const supabase = await createServerClient()
const { data: { user } } = await supabase.auth.getUser()
const { data: profile } = await supabase
.from('users')
.select('stripe_customer_id')
.eq('id', user?.id)
.single()
const session = await stripe.billingPortal.sessions.create({
customer: profile.stripe_customer_id,
return_url: `${process.env.NEXT_PUBLIC_APP_URL}/account`,
})
return Response.json({ url: session.url })
}STRIPE_WEBHOOK_SECRETprice_id exists