KitRocket

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

DodoPaymentsStripe
Tax handlingAutomatic (MoR)You handle it (or use Stripe Tax)
ComplianceHandled for youYour responsibility
SetupSimpleMore configuration
Global coverage135+ countries46+ countries
FeesVaries by region2.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

  1. Go to Stripe Dashboard
  2. Create products for each plan (Starter, Pro)
  3. Add recurring prices (monthly and/or yearly)
  4. 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

  1. Go to Stripe Webhooks
  2. Add endpoint: https://yourdomain.com/api/webhook/stripe
  3. Select events: checkout.session.completed, customer.subscription.updated, customer.subscription.deleted
  4. 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

  1. Delete src/app/api/webhook/dodo/ directory
  2. Remove DodoPayments environment variables
  3. Remove old dependencies if any DodoPayments SDK was used
  4. Test the full checkout flow end-to-end

On this page