Payments Module
DodoPayments integration — subscriptions, one-time payments, and Merchant of Record out of the box.
Overview
KitRocket uses DodoPayments as a Merchant of Record (MoR). This means DodoPayments handles:
- Payment processing in 135+ countries
- Sales tax, VAT, and GST collection and remittance
- Compliance and invoicing
- Refunds and chargebacks
You don't need to register as a tax entity or worry about global compliance. DodoPayments acts as the seller on your behalf.
The payments module supports:
- Recurring subscriptions (monthly/yearly)
- One-time payments
- Plan management and upgrades
- Webhook-driven status updates
All payment logic lives in src/lib/payments/.
Configuration
Environment variables
DODO_API_KEY="your-dodo-api-key"
DODO_WEBHOOK_SECRET="your-webhook-signing-secret"
DODO_STARTER_PRICE_ID="price_starter_monthly"
DODO_PRO_PRICE_ID="price_pro_monthly"
Create products in DodoPayments
- Log in to dodopayments.com
- Go to Products > Create Product
- Create your subscription plans with pricing
- Copy the price IDs into your
.env.local
Plan definitions
Plans are defined in a single file for easy editing:
export const PLANS = {
starter: {
name: "Starter",
priceId: process.env.DODO_STARTER_PRICE_ID!,
price: 29,
features: [
"Up to 1,000 users",
"Basic analytics",
"Email support",
],
},
pro: {
name: "Pro",
priceId: process.env.DODO_PRO_PRICE_ID!,
price: 79,
features: [
"Unlimited users",
"Advanced analytics",
"Priority support",
"AI features",
],
},
} as const;
Usage
Create a checkout session
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth/server";
import { createCheckout } from "@/lib/payments/checkout";
export async function POST(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { priceId } = await request.json();
const checkout = await createCheckout({
customerEmail: session.user.email,
priceId,
successUrl: `${process.env.BETTER_AUTH_URL}/dashboard?checkout=success`,
cancelUrl: `${process.env.BETTER_AUTH_URL}/billing`,
});
return NextResponse.json({ url: checkout.url });
}
Handle webhooks
import { NextRequest, NextResponse } from "next/server";
import { verifyWebhook, handleWebhookEvent } from "@/lib/payments/webhook";
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("x-dodo-signature");
const isValid = verifyWebhook(body, signature);
if (!isValid) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
const event = JSON.parse(body);
await handleWebhookEvent(event);
return NextResponse.json({ received: true });
}
The webhook handler processes these events:
export async function handleWebhookEvent(event: WebhookEvent) {
switch (event.type) {
case "payment.completed":
// Activate subscription
break;
case "subscription.updated":
// Update plan or status
break;
case "subscription.cancelled":
// Mark subscription as cancelled
break;
}
}
Check subscription status
import { db } from "@/lib/db";
import { subscriptions } from "@/db/schema/subscriptions";
import { eq } from "drizzle-orm";
export async function getUserSubscription(userId: string) {
const result = await db
.select()
.from(subscriptions)
.where(eq(subscriptions.userId, userId))
.limit(1);
return result[0] ?? null;
}
Gate features by plan
import { getUserSubscription } from "@/lib/payments/dodo";
export default async function DashboardPage() {
const subscription = await getUserSubscription(session.user.id);
const isPro = subscription?.plan === "pro" && subscription?.status === "active";
return (
<div>
{isPro ? <ProFeatures /> : <StarterFeatures />}
</div>
);
}
Customization
Add a new plan
- Create the product in DodoPayments dashboard
- Add the price ID to
.env.local - Add the plan to
src/lib/payments/plans.ts - Update the pricing component in
src/components/landing/pricing.tsx
Usage-based billing
DodoPayments supports metered billing. Track usage in your database and report it:
export async function reportUsage(subscriptionId: string, quantity: number) {
// Report usage to DodoPayments API
}
Custom webhook handlers
Add new event types in src/lib/payments/webhook.ts. DodoPayments sends events for disputes, refunds, and more.
Removing this module
If you want to use a different payment provider:
- Delete
src/lib/payments/directory - Delete
src/app/api/checkout/route.ts - Delete
src/app/api/webhook/dodo/route.ts - Remove billing page at
src/app/(dashboard)/billing/ - Drop the
subscriptionstable from your schema - Remove DodoPayments environment variables
- See Stripe Migration if switching to Stripe