@liveedevteam/stripe
Stripe integration module for Next.js App Router + Supabase — one-time payments, subscriptions, webhooks, and server actions.
Claude Code setup skill
This package ships a Claude Code skill that automates the full integration — migration, webhook route, env vars, and more. No copy-pasting required.
How to use it
After installing the package, open Claude Code in your project and type:
set up stripe
Claude detects what's already in place and walks through every step automatically.
What the skill does:
- Checks for missing dependencies and installs them
- Creates the Supabase migration (
stripe_customers,subscriptions,orders,products,prices,webhook_events) and applies it - Creates
app/api/webhooks/stripe/route.tswith signature verification and idempotency - Scaffolds a
CheckoutButtoncomponent (and billing portal button if subscriptions are enabled) - Adds all required env vars to
.env.local(with placeholder values to fill in) - Optionally configures Slack failure notifications
- Warns you to run the backfill script if existing users are present
The skill file (SKILL.md) ships inside the package — Claude Code reads it automatically from node_modules/@liveedevteam/stripe/SKILL.md. You get updated instructions with every version you install.
Example session
You: set up stripe
Claude: Running preflight checks...
✓ @liveedevteam/stripe installed
✓ stripe installed
✓ @supabase/ssr installed
✗ Supabase migration not found
Creating migration...
Writing supabase/migrations/20260101000000_create_stripe_tables.sql
Running: supabase db push
Creating webhook route...
Writing app/api/webhooks/stripe/route.ts
Adding env vars to .env.local...
STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
Done. Next steps:
1. Fill in the placeholder values in .env.local
2. Run: stripe listen --forward-to localhost:3000/api/webhooks/stripe
3. Test: stripe trigger checkout.session.completed
Requirements
- Next.js 14+
- Supabase (Auth + Postgres)
- Node.js 18+
Installation
pnpm add @liveedevteam/stripe stripe @supabase/ssrEnvironment variables
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=xxx
SUPABASE_SERVICE_ROLE_KEY=xxxDatabase migration
supabase migration new create_stripe_tablesPaste into the generated file:
create table stripe_customers (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) not null,
stripe_customer_id text unique not null,
created_at timestamptz default now()
);
create table subscriptions (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id) not null,
stripe_subscription_id text unique not null,
stripe_price_id text not null,
status text not null,
current_period_start timestamptz,
current_period_end timestamptz,
cancel_at_period_end boolean default false,
cancel_at timestamptz,
created_at timestamptz default now()
);
create table orders (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users(id), -- nullable for anonymous payments
stripe_session_id text unique not null,
amount integer not null,
currency text not null,
status text not null,
created_at timestamptz default now()
);
create table webhook_events (
id text primary key,
type text not null,
processed_at timestamptz default now()
);
-- Row Level Security
alter table stripe_customers enable row level security;
alter table subscriptions enable row level security;
alter table orders enable row level security;
alter table webhook_events enable row level security;
create policy "users_read_own_stripe_customer" on stripe_customers
for select to authenticated using (auth.uid() = user_id);
create policy "users_read_own_subscriptions" on subscriptions
for select to authenticated using (auth.uid() = user_id);
create policy "users_read_own_orders" on orders
for select to authenticated using (auth.uid() = user_id);supabase db pushWebhook route
// app/api/webhooks/stripe/route.ts
import { createWebhookHandler } from '@liveedevteam/stripe/webhooks'
export const POST = createWebhookHandler()With optional Slack notifications on failure:
export const POST = createWebhookHandler({
slack: {
webhookUrl: process.env.SLACK_WEBHOOK_URL!,
channel: '#payments-alerts',
},
})Server actions
Checkout button
'use client'
import { createCheckout } from '@liveedevteam/stripe/actions'
export const CheckoutButton = ({ priceId, mode }: {
priceId: string
mode: 'payment' | 'subscription'
}) => (
<form action={() => createCheckout(priceId, mode)}>
<button type="submit">Checkout</button>
</form>
)Billing portal
'use client'
import { getBillingPortal } from '@liveedevteam/stripe/actions'
export const BillingPortalButton = () => (
<form action={getBillingPortal}>
<button type="submit">Manage Subscription</button>
</form>
)Guard a page
import { requireActiveSubscription } from '@liveedevteam/stripe/actions'
export default async function DashboardPage() {
await requireActiveSubscription() // redirects to /pricing if not active
return <div>...</div>
}Check subscription status
import { getSubscription } from '@liveedevteam/stripe/actions'
const subscription = await getSubscription() // null for anonymous or no subscription
if (subscription?.status === 'active' || subscription?.status === 'trialing') {
// has access
}Cancel a subscription
import { cancelSubscription } from '@liveedevteam/stripe/actions'
// Cancel at period end (default) — user keeps access until billing period ends
await cancelSubscription()
// Cancel immediately
await cancelSubscription(true)The DB is updated automatically via customer.subscription.updated / customer.subscription.deleted webhooks.
Upgrade or downgrade
import { changeSubscription } from '@liveedevteam/stripe/actions'
await changeSubscription('price_new_plan_id')
// Control proration
await changeSubscription('price_new_plan_id', 'none') // no proration
await changeSubscription('price_new_plan_id', 'always_invoice') // invoice immediatelyThe DB is updated automatically via customer.subscription.updated webhook.
TypeScript types
import type { Subscription, Database } from '@liveedevteam/stripe/types'Subscription is derived directly from the Database schema so it stays in sync with your table.
Anonymous user support
| Action | Anonymous |
|---|---|
createCheckout('payment') |
Allowed — order recorded with user_id = null |
createCheckout('subscription') |
Throws Unauthorized |
getBillingPortal() |
Throws Unauthorized |
getSubscription() |
Returns null |
requireActiveSubscription() |
Redirects to /pricing |
cancelSubscription() |
Throws Unauthorized |
changeSubscription(priceId) |
Throws Unauthorized |
Local testing
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completedTesting
import { buildWebhookRequest, stripeFixtures } from '@liveedevteam/stripe/testing'Build a signed webhook request to pass directly to your route handler in tests:
const req = buildWebhookRequest(
'checkout.session.completed',
stripeFixtures.checkoutSessionCompleted({ mode: 'subscription', userId: 'user-1' }),
{ secret: process.env.STRIPE_WEBHOOK_SECRET! }
)
const res = await POST(req)
expect(res.status).toBe(200)Available fixtures: checkoutSessionCompleted, subscription, invoice. All are shaped for Stripe API version 2026-05-27.dahlia.
Existing users backfill
If users already had Stripe subscriptions before you installed this package, sync them into the stripe_customers table:
node node_modules/@liveedevteam/stripe/dist/scripts/backfill.jsThe script looks up each user by email in Stripe and records the match — it does not create new Stripe customers. Always run against staging first.
License
MIT