build strategy · dodo

Live payments, zero PCI, one build.

Every mega-prompt in this archive collapses into the same shape: one TanStack server function that calls the Dodo Node SDK, one public webhook route that verifies the Svix signature, one client surface that redirects to the hosted payment link. That's the whole app.

Why Dodo hosts the checkout?

dodo.payments.create({ payment_link: true }) returns a URL you redirect the buyer to. Dodo renders the card fields, handles 3DS, routes the money — you never touch card data, so PCI scope stays at zero. Your app only deals with an opaque payment_id and a webhook.

Why one webhook route?

Dodo signs every event with Svix headers (webhook-id, webhook-timestamp, webhook-signature). One tiny /api/public/dodo-webhook route verifies the signature with DODO_WEBHOOK_SECRET, then flips the unlock — a cookie, a DB row, a queue message, whatever your product needs.

The whole checkout, one file.

A TanStack server function that mints a Dodo hosted payment link. Your client calls it, gets a URL, redirects. That's the entire buy flow.

// src/lib/dodo-checkout.functions.ts — mint a hosted Dodo payment link
// Built during the Creative AI & Quantum Hackathon — StreetKode Fam · Indian Krump Festival 14
import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";
import DodoPayments from "dodopayments";

export const startCheckout = createServerFn({ method: "POST" })
  .inputValidator((d) => z.object({ productName: z.string().min(1) }).parse(d))
  .handler(async ({ data }) => {
    const dodo = new DodoPayments({
      bearerToken: process.env.DODO_API_KEY!,
      environment: process.env.DODO_MODE === "live" ? "live_mode" : "test_mode",
    });
    const payment = await dodo.payments.create({
      payment_link: true,
      billing: { city: "-", country: "US", state: "-", street: "-", zipcode: "-" },
      customer: { email: "buyer@example.com", name: "Buyer" },
      product_cart: [{ product_id: "prod_prompt_pack", quantity: 1 }],
      return_url: "https://your-app.lovable.app/showcase/checkout-return",
    });
    return { payment_id: payment.payment_id, payment_link: payment.payment_link };
  });

The whole unlock, one route.

Verify the Svix headers with standardwebhooks (Dodo uses the Svix standard). On payment.succeeded, set an httpOnly cookie the return page reads to reveal the download.

// src/routes/api/public/dodo-webhook.ts — verify the Svix signature, flip the unlock
import { createFileRoute } from "@tanstack/react-router";
import { Webhook } from "standardwebhooks";

export const Route = createFileRoute("/api/public/dodo-webhook")({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const body = await request.text();
        const wh = new Webhook(process.env.DODO_WEBHOOK_SECRET!);
        const event = wh.verify(body, {
          "webhook-id": request.headers.get("webhook-id")!,
          "webhook-timestamp": request.headers.get("webhook-timestamp")!,
          "webhook-signature": request.headers.get("webhook-signature")!,
        }) as { type: string; data: { payment_id: string } };

        if (event.type === "payment.succeeded") {
          return new Response("ok", {
            headers: {
              "Set-Cookie": `dodo_unlocked=${event.data.payment_id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=3600`,
            },
          });
        }
        return new Response("ignored");
      },
    },
  },
});

Two secrets. Zero infra.

# .env — two secrets, nothing else
DODO_API_KEY=your-dodo-test-key
DODO_WEBHOOK_SECRET=whsec_...

# Optional:
# DODO_MODE=live      # switch to live_mode; omit / set 'test' to stay in test_mode

# How Lovable wires this up in one prompt:
# 1. Paste a mega-prompt from this archive.
# 2. Lovable
#    - runs bun add dodopayments standardwebhooks
#    - writes the server function that calls dodo.payments.create()
#    - writes /api/public/dodo-webhook that verifies the Svix signature
#    - writes the client surface that redirects to the hosted payment link
# 3. Add the two secrets in Settings → Secrets. Done.