| name | nexty-payments |
| description | Implement payments in NEXTY.DEV with Stripe or Creem. Use when creating checkout sessions, handling webhooks, managing subscriptions, or working with credits. Covers provider-agnostic patterns and webhook handlers. |
Payments in NEXTY.DEV
Overview
- Providers: Stripe, Creem
- Credit Manager:
lib/payments/credit-manager.ts
- Webhook Helpers:
lib/payments/webhook-helpers.ts
- Provider Utils:
lib/payments/provider-utils.ts
Creating Checkout Sessions
Stripe Checkout
import Stripe from 'stripe';
import { stripe } from '@/lib/stripe';
import { getOrCreateStripeCustomer } from '@/actions/stripe';
export async function createStripeCheckout(planId: string, userId: string) {
const customerId = await getOrCreateStripeCustomer(userId);
const session = await stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: 'price_xxx', quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/payment/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_SITE_URL}/pricing`,
metadata: {
userId,
planId,
},
});
return session.url;
}
Creem Checkout
import { createCreemCheckoutSession } from '@/lib/creem/client';
export async function createCreemCheckout(planId: string, userId: string, email: string) {
const checkout = await createCreemCheckoutSession({
product_id: 'prod_xxx',
success_url: `${process.env.NEXT_PUBLIC_SITE_URL}/payment/success`,
request_id: `${userId}-${Date.now()}`,
metadata: {
userId,
planId,
},
customer: { email },
});
return checkout.checkout_url;
}
Handling Webhooks
Stripe Webhook Pattern
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
import { handleCheckoutSessionCompleted, handleInvoicePaid } from './webhook-handlers';
export async function POST(request: Request) {
const body = await request.text();
const headersList = await headers();
const signature = headersList.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err: any) {
console.error('Webhook signature verification failed:', err.message);
return new Response('Webhook Error', { status: 400 });
}
try {
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutSessionCompleted(event.data.object);
break;
case 'invoice.paid':
await handleInvoicePaid(event.data.object);
break;
}
} catch (error) {
console.error('Webhook handler error:', error);
return new Response('Webhook handler failed', { status: 500 });
}
return new Response('OK', { status: 200 });
}
Creem Webhook Pattern
import { headers } from 'next/headers';
import { handleCreemPaymentSucceeded, handleCreemInvoicePaid } from './handlers';
export async function POST(request: Request) {
const body = await request.text();
const headersList = await headers();
const signature = headersList.get('creem-signature');
const payload = JSON.parse(body);
switch (payload.eventType) {
case 'checkout.completed':
await handleCreemPaymentSucceeded(payload);
break;
case 'subscription.paid':
await handleCreemInvoicePaid(payload);
break;
}
return new Response('OK', { status: 200 });
}
Credit Management
Grant Credits (One-Time Purchase)
import { upgradeOneTimeCredits } from '@/lib/payments/credit-manager';
await upgradeOneTimeCredits(userId, planId, orderId);
Grant Subscription Credits
import { upgradeSubscriptionCredits } from '@/lib/payments/credit-manager';
await upgradeSubscriptionCredits(
userId,
planId,
orderId,
currentPeriodStart
);
Revoke Credits (Refund)
import { revokeOneTimeCredits, revokeSubscriptionCredits } from '@/lib/payments/credit-manager';
await revokeOneTimeCredits(refundAmountCents, originalOrder, refundOrderId);
await revokeSubscriptionCredits(originalOrder);
Deduct Credits (Usage)
import { deductCredits } from '@/actions/usage/deduct';
const result = await deductCredits(10, 'AI generation - 10 credits');
if (!result.success) {
toast.error(result.error);
}
Get User Benefits
import { getUserBenefits } from '@/actions/usage/benefits';
import { getClientUserBenefits } from '@/actions/usage/deduct';
const benefits = await getUserBenefits(userId);
const result = await getClientUserBenefits();
if (result.success) {
const { oneTimeCredits, subscriptionCredits, totalCredits } = result.data;
}
Order Management
Create Order with Idempotency
import { createOrderWithIdempotency } from '@/lib/payments/webhook-helpers';
const result = await createOrderWithIdempotency('stripe', {
userId,
providerOrderId: paymentIntentId,
orderType: 'one_time',
status: 'completed',
planId,
amountTotal: amount,
currency: 'usd',
metadata: { ... },
}, `stripe-${paymentIntentId}`);
if (result.alreadyExists) {
return;
}
Query Orders
import { db } from '@/lib/db';
import { orders } from '@/lib/db/schema';
import { eq, desc } from 'drizzle-orm';
const userOrders = await db.select()
.from(orders)
.where(eq(orders.userId, userId))
.orderBy(desc(orders.createdAt));
Subscription Management
Create Customer Portal Session
import { createStripePortalSession } from '@/actions/stripe';
const portalUrl = await createStripePortalSession();
import { createCreemPortalSession } from '@/actions/creem/portal';
const portalUrl = await createCreemPortalSession();
Query Subscriptions
import { db } from '@/lib/db';
import { subscriptions } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
const [subscription] = await db.select()
.from(subscriptions)
.where(eq(subscriptions.userId, userId))
.limit(1);
if (subscription?.status === 'active') {
}
Pricing Plans
Query Plans
import { db } from '@/lib/db';
import { pricingPlans } from '@/lib/db/schema';
import { eq, and } from 'drizzle-orm';
const plans = await db.select()
.from(pricingPlans)
.where(
and(
eq(pricingPlans.isActive, true),
eq(pricingPlans.environment, 'live')
)
)
.orderBy(pricingPlans.displayOrder);
Plan Benefits Structure
{
"oneTimeCredits": 100,
"monthlyCredits": 50,
"totalMonths": 12,
"customFeature": true
}
Payment Type Helpers
import {
isSubscriptionOrder,
isOneTimePurchase,
isMonthlyInterval,
isYearlyInterval,
normalizeRecurringInterval,
} from '@/lib/payments/provider-utils';
if (isSubscriptionOrder(order.orderType)) {
}
if (isOneTimePurchase(order.orderType)) {
}
if (isMonthlyInterval(subscription.interval)) {
}
Environment Variables
# Stripe
STRIPE_SECRET_KEY
STRIPE_WEBHOOK_SECRET
STRIPE_CUSTOMER_PORTAL_URL
# Creem
CREEM_API_KEY
CREEM_WEBHOOK_SECRET
CREEM_API_BASE_URL
Money Calculation
⚠️ Use integers (cents) for all calculations - JS floats lose precision (19.99 * 3 = 59.97000000000001)
import { toCurrencyAmount, toCents } from '@/lib/payments/webhook-helpers';
toCurrencyAmount(1999);
toCents("19.99");
See ./money-calculation.md for details
Checklist
- Store
userId and planId in checkout metadata
- Use
createOrderWithIdempotency to prevent duplicates
- Handle all relevant webhook events
- Use credit manager for consistent credit operations
- Log errors with context (userId, orderId)
- Test webhooks locally with CLI tools
- Verify webhook signatures before processing
- Use integers (cents) for money calculations