KitRocket

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

  1. Log in to dodopayments.com
  2. Go to Products > Create Product
  3. Create your subscription plans with pricing
  4. 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

  1. Create the product in DodoPayments dashboard
  2. Add the price ID to .env.local
  3. Add the plan to src/lib/payments/plans.ts
  4. 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:

  1. Delete src/lib/payments/ directory
  2. Delete src/app/api/checkout/route.ts
  3. Delete src/app/api/webhook/dodo/route.ts
  4. Remove billing page at src/app/(dashboard)/billing/
  5. Drop the subscriptions table from your schema
  6. Remove DodoPayments environment variables
  7. See Stripe Migration if switching to Stripe

On this page