Add payments to a Next.js + Supabase project:
Install Stripe:
npm install stripe @stripe/stripe-js
Add env vars (see quick_reference below)
Create idempotency table (see schema below)
Choose workflow:
setup-new-project.md
or
add-webhook-handler.md
// Lazy-loaded Stripe client
import
Stripe
from
'stripe'
;
let
stripe
:
Stripe
|
null
=
null
;
export
function
getStripe
(
)
:
Stripe
{
if
(
!
_stripe
)
{
_stripe
=
new
Stripe
(
process
.
env
.
STRIPE_SECRET_KEY
!
,
{
apiVersion
:
'2025-12-15.clover'
}
)
;
}
return
_stripe
;
}
Integration is successful when:
Webhook handler uses database-backed idempotency (not in-memory)
All keys in environment variables (never hardcoded)
Test mode fully working before any live mode deployment
Signature verification on all webhook endpoints
Event logging before processing (insert-before-process pattern)
Go-live checklist completed before production deployment
Core Principles
Idempotency is Non-Negotiable
ALL webhook handlers MUST use database-backed idempotency
Never use in-memory Sets (lost on serverless cold starts)
Insert event record BEFORE processing, not after
Test/Live Mode Separation
Use environment variables for ALL keys (never hardcode)
Test keys:
sk_test
,
pk_test_
,
whsec_test_
Live keys:
sk_live_
,
pk_live_
,
whsec_live_
Products/prices must be recreated in live mode
Shared Stripe Account
All NetZero Suite projects share ONE Stripe account
Same webhook secret can be used across projects
Each project has its own webhook endpoint URL
Lazy Client Initialization
Never initialize Stripe at module level (build errors)
Use factory function pattern for server-side client
Check for API key before creating instance
What Are You Building?
Before proceeding, identify your use case:
Use Case
Workflow
Description
New project
setup-new-project.md
Fresh Stripe integration from scratch
Add webhooks
add-webhook-handler.md
Add webhook handler to existing project
Subscriptions
implement-subscriptions.md
Recurring billing with plans
Credit system
add-credit-system.md
Pay-as-you-go credits
Go live
go-live-checklist.md
Test → Production migration
Workflow Routing
If setting up Stripe in a new project:
→ Read
workflows/setup-new-project.md
→ Then read
reference/environment-vars.md
→ Use
templates/stripe-client.ts
and
templates/env-example.txt
If adding webhook handling:
→ Read
workflows/add-webhook-handler.md
→ Then read
reference/webhook-patterns.md
→ Use
templates/webhook-handler-nextjs.ts
and
templates/idempotency-migration.sql
If implementing subscription billing:
→ Read
workflows/implement-subscriptions.md
→ Then read
reference/pricing-models.md
→ Use
templates/plans-config.ts
If adding credit/usage-based system:
→ Read
workflows/add-credit-system.md
→ Then read
reference/pricing-models.md
If migrating test → production:
→ Read
workflows/go-live-checklist.md
Quick Reference
Environment Variables (Standard)
Server-side (never expose to client)
STRIPE_SECRET_KEY
sk_test_
..
.
STRIPE_WEBHOOK_SECRET
=
whsec_
..
.
Client-side (safe to expose)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
pk_test_
..
.
Optional: Price IDs (for test→live switching)
STRIPE_PRICE_STARTER_MONTHLY
- price_
- ..
- .
- STRIPE_PRICE_PRO_MONTHLY
- =
- price_
- ..
- .
- Common Webhook Events
- Event
- When It Fires
- Action
- checkout.session.completed
- Customer completes checkout
- Create subscription record
- customer.subscription.created
- New subscription starts
- Initialize user limits
- customer.subscription.updated
- Plan change, renewal
- Update plan/limits
- customer.subscription.deleted
- Cancellation
- Downgrade to free
- invoice.paid
- Monthly renewal success
- Reset usage counters
- invoice.payment_failed
- Payment failed
- Mark as past_due
- Stripe Client Pattern
- let
- _stripe
- :
- Stripe
- |
- null
- =
- null
- ;
- export
- function
- getStripe
- (
- )
- :
- Stripe
- {
- if
- (
- !
- _stripe
- )
- {
- const
- key
- =
- process
- .
- env
- .
- STRIPE_SECRET_KEY
- ;
- if
- (
- !
- key
- )
- throw
- new
- Error
- (
- 'STRIPE_SECRET_KEY not configured'
- )
- ;
- _stripe
- =
- new
- Stripe
- (
- key
- ,
- {
- apiVersion
- :
- '2025-12-15.clover'
- ,
- typescript
- :
- true
- }
- )
- ;
- }
- return
- _stripe
- ;
- }
- Idempotency Table Schema
- CREATE
- TABLE
- stripe_webhook_events
- (
- id
- TEXT
- PRIMARY
- KEY
- ,
- -- Use Stripe event ID directly
- type
- TEXT
- NOT
- NULL
- ,
- -- Event type
- data
- JSONB
- NOT
- NULL
- ,
- -- Full event payload
- processed_at TIMESTAMPTZ
- DEFAULT
- NOW
- (
- )
- )
- ;
- Webhook Handler Structure
- export
- async
- function
- POST
- (
- request
- :
- NextRequest
- )
- {
- const
- body
- =
- await
- request
- .
- text
- (
- )
- ;
- const
- signature
- =
- request
- .
- headers
- .
- get
- (
- 'stripe-signature'
- )
- ;
- // 1. Verify signature
- const
- event
- =
- stripe
- .
- webhooks
- .
- constructEvent
- (
- body
- ,
- signature
- ,
- webhookSecret
- )
- ;
- // 2. Check idempotency (BEFORE processing)
- const
- {
- data
- :
- existing
- }
- =
- await
- supabase
- .
- from
- (
- 'stripe_webhook_events'
- )
- .
- select
- (
- 'id'
- )
- .
- eq
- (
- 'id'
- ,
- event
- .
- id
- )
- .
- single
- (
- )
- ;
- if
- (
- existing
- )
- return
- NextResponse
- .
- json
- (
- {
- duplicate
- :
- true
- }
- )
- ;
- // 3. Log event (INSERT before processing)
- await
- supabase
- .
- from
- (
- 'stripe_webhook_events'
- )
- .
- insert
- (
- {
- id
- :
- event
- .
- id
- ,
- type
- :
- event
- .
- type
- ,
- data
- :
- event
- ,
- }
- )
- ;
- // 4. Process event
- switch
- (
- event
- .
- type
- )
- {
- case
- 'checkout.session.completed'
- :
- await
- handleCheckout
- (
- event
- .
- data
- .
- object
- )
- ;
- break
- ;
- // ... other handlers
- }
- return
- NextResponse
- .
- json
- (
- {
- received
- :
- true
- }
- )
- ;
- }
- Integration Notes
- Works With
- Supabase
-
- Use service role client for webhook handlers (bypasses RLS)
- Prisma
-
- Alternative to Supabase for idempotency table
- Vercel
-
- Add runtime/maxDuration config for webhook routes
- Next.js App Router
- Use
request.text()
for raw body