Migrate to Stripe
Replace DodoPayments with Stripe for payment processing.
KitRocket ships with DodoPayments, but some teams prefer Stripe. This guide walks through the full migration.
When to use Stripe vs DodoPayments
| DodoPayments | Stripe | |
|---|---|---|
| Tax handling | Automatic (MoR) | You handle it (or use Stripe Tax) |
| Compliance | Handled for you | Your responsibility |
| Setup | Simple | More configuration |
| Global coverage | 135+ countries | 46+ countries |
| Fees | Varies by region | 2.9% + 30c (US) |
DodoPayments is simpler because it's a Merchant of Record. Stripe gives you more control but more responsibility.
Step 1: Install Stripe SDK
pnpm add stripe @stripe/stripe-js
Step 2: Update environment variables
Replace DodoPayments variables with Stripe ones:
# Remove these:
# DODO_API_KEY=
# DODO_WEBHOOK_SECRET=
# DODO_STARTER_PRICE_ID=
# DODO_PRO_PRICE_ID=
# Add these:
STRIPE_SECRET_KEY="sk_test_..."
STRIPE_PUBLISHABLE_KEY="pk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_STARTER_PRICE_ID="price_..."
STRIPE_PRO_PRICE_ID="price_..."
Step 3: Create Stripe products
- Go to Stripe Dashboard
- Create products for each plan (Starter, Pro)
- Add recurring prices (monthly and/or yearly)
- Copy the price IDs into
.env.local
Step 4: Update the payment client
Replace the DodoPayments client with Stripe:
import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-12-18.acacia",
});
Step 5: Update checkout
Replace the checkout session creation:
import { stripe } from "./stripe";
interface CreateCheckoutParams {
customerEmail: string;
priceId: string;
successUrl: string;
cancelUrl: string;
}
export async function createCheckout({
customerEmail,
priceId,
successUrl,
cancelUrl,
}: CreateCheckoutParams) {
const session = await stripe.checkout.sessions.create({
mode: "subscription",
customer_email: customerEmail,
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl,
cancel_url: cancelUrl,
});
return { url: session.url };
}
Step 6: Update the checkout API route
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 });
}
Step 7: Update webhook handler
Replace the DodoPayments webhook with a Stripe one:
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/payments/stripe";
import { handleWebhookEvent } from "@/lib/payments/webhook";
import type Stripe from "stripe";
export async function POST(request: NextRequest) {
const body = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "Missing signature" }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
await handleWebhookEvent(event);
return NextResponse.json({ received: true });
}
Update the webhook handler:
import type Stripe from "stripe";
import { db } from "@/lib/db";
import { subscriptions } from "@/db/schema/subscriptions";
import { eq } from "drizzle-orm";
export async function handleWebhookEvent(event: Stripe.Event) {
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
await db.insert(subscriptions).values({
userId: session.metadata?.userId ?? "",
plan: "starter", // determine from price ID
status: "active",
stripeCustomerId: session.customer as string,
stripeSubscriptionId: session.subscription as string,
});
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object as Stripe.Subscription;
await db
.update(subscriptions)
.set({ status: subscription.status })
.where(eq(subscriptions.stripeSubscriptionId, subscription.id));
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object as Stripe.Subscription;
await db
.update(subscriptions)
.set({ status: "cancelled" })
.where(eq(subscriptions.stripeSubscriptionId, subscription.id));
break;
}
}
}
Step 8: Update the database schema
Rename DodoPayments columns to Stripe:
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
export const subscriptions = pgTable("subscriptions", {
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
plan: text("plan").notNull(),
status: text("status").notNull(),
stripeCustomerId: text("stripe_customer_id"),
stripeSubscriptionId: text("stripe_subscription_id"),
createdAt: timestamp("created_at").defaultNow(),
});
Push the schema change:
pnpm db:push
Step 9: Update plan definitions
export const PLANS = {
starter: {
name: "Starter",
priceId: process.env.STRIPE_STARTER_PRICE_ID!,
price: 29,
features: ["Up to 1,000 users", "Basic analytics", "Email support"],
},
pro: {
name: "Pro",
priceId: process.env.STRIPE_PRO_PRICE_ID!,
price: 79,
features: ["Unlimited users", "Advanced analytics", "Priority support"],
},
};
Step 10: Set up Stripe webhook
- Go to Stripe Webhooks
- Add endpoint:
https://yourdomain.com/api/webhook/stripe - Select events:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted - Copy the signing secret to
STRIPE_WEBHOOK_SECRET
For local testing, use the Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhook/stripe
Step 11: Clean up
- Delete
src/app/api/webhook/dodo/directory - Remove DodoPayments environment variables
- Remove old dependencies if any DodoPayments SDK was used
- Test the full checkout flow end-to-end