ignitionstack.pro v1.0 is out! Read the announcement →
Skip to Content

Stripe Integration

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.

Architecture Overview

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 status

Environment Variables

Create a Stripe account

Go to dashboard.stripe.com  and create an account. Keep Test Mode enabled for development.

Get API keys

No Dashboard → Developers → API Keys:

.env.local
# Stripe Keys STRIPE_SECRET_KEY=sk_test_... NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # Webhook Secret (configure em Webhooks) STRIPE_WEBHOOK_SECRET=whsec_...

Create products and prices

  1. Open Products in the dashboard
  2. Create products with the prices you need
  3. Copy the price_id for checkout
// Usage example const priceId = 'price_1234567890'

Webhook configuration

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/webhooks

The CLI prints the webhook secret (whsec_...) you must copy into .env.local.

Checkout implementation

Server Action

src/app/actions/stripe/create-checkout.ts
'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 } }

Componente de Checkout

src/app/components/checkout-button.tsx
'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> ) }

Webhook Handler

src/app/api/stripe/webhooks/route.ts
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 }) }

Affiliate Promotion Codes

Affiliates get their own coupon + promotion code automatically during signup:

  1. The server action src/app/actions/affiliates/become-affiliate.ts creates a Stripe coupon (AFFILIATE_<CODE>) with the affiliate discount rate.
  2. Right after that it creates a promotion code using the actual affiliate code (so they can share the same value they see in the dashboard).
  3. Both identifiers (stripe_coupon_id and stripe_promotion_code_id) are stored on the affiliates table for future reference.

Stripe Dashboard → MarketingCoupons / 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 the stripe_promotion_code_id column exists before deploying.

Testing the flow

Always test with Stripe’s test cards before going live!

Test cards

ScenarioCard number
Success4242 4242 4242 4242
Requires authentication4000 0025 0000 3155
Payment declined4000 0000 0000 9995

Validation checklist

Customer Portal (subscriptions)

For subscriptions, enable the Customer Portal inside the Stripe Dashboard and implement:

src/app/api/stripe/portal/route.ts
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 }) }

Troubleshooting

Webhook is not received

Checkout does not redirect

Webhook signature error

Resources