DEV Community

Dasha Dagayeva
Dasha Dagayeva

Posted on • Originally published at dashbuilds.dev

How I Set Up Stripe Checkout in 2 Hours Without a Backend

I needed to sell 9 digital products on a static site. No Next.js. No Express. No backend framework at all.

Just HTML pages on Vercel and Stripe Checkout handling the payment UI.

Here's the entire architecture: 3 serverless functions. That's it.

The Problem

I had 9 developer kits ($14–$59) ready to sell. I needed:

  • A checkout flow that creates Stripe sessions
  • Sale notifications via email
  • A success page that shows the right download links

I didn't want to spin up a backend framework for what's essentially 3 API endpoints.

The Architecture

Browser → POST /api/checkout → Stripe Checkout Session → Stripe hosted page
                                                              ↓
                                                        Customer pays
                                                              ↓
Stripe webhook → POST /api/webhook → Verify signature → Send email via Resend
                                                              ↓
Success page → GET /api/session → Fetch session → Show download links
Enter fullscreen mode Exit fullscreen mode

Three files. No framework. No database for orders (Stripe is the database).

File 1: api/checkout.js

Creates a Stripe Checkout Session. The product slug comes from the button click, gets mapped to a Stripe Price ID.

// api/checkout.js
export default async function handler(req, res) {
  const { product } = req.body;

  const priceMap = {
    'brain-kit': 'price_1TIrnk...',
    'invoice-kit': 'price_1TIrst...',
    'dash-biz-suite': 'price_1TIrsz...',
    // ... 9 products total
  };

  const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      'line_items[0][price]': priceMap[product],
      'line_items[0][quantity]': '1',
      'mode': 'payment',
      'success_url': `https://dashbuilds.dev/success?session_id={CHECKOUT_SESSION_ID}`,
      'cancel_url': 'https://dashbuilds.dev/shop',
      'metadata[product]': product,
    }),
  });

  const session = await response.json();
  res.json({ url: session.url });
}
Enter fullscreen mode Exit fullscreen mode

No Stripe SDK. Just fetch to the Stripe API with URL-encoded params. The SDK is nice but it's a dependency I didn't need for one endpoint.

File 2: api/webhook.js

Stripe sends a checkout.session.completed event after payment. This function verifies the signature and sends me a notification email via Resend.

// api/webhook.js
import { buffer } from 'micro';
import Stripe from 'stripe';

export default async function handler(req, res) {
  const buf = await buffer(req);
  const sig = req.headers['stripe-signature'];

  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
  const event = stripe.webhooks.constructEvent(
    buf, sig, process.env.STRIPE_WEBHOOK_SECRET
  );

  if (event.type === 'checkout.session.completed') {
    const session = event.data.object;

    // Send notification via Resend
    await fetch('https://api.resend.com/emails', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        from: 'Dash Builds <sales@dashbuilds.dev>',
        to: 'dash@dashbuilds.dev',
        subject: `New sale: ${session.metadata.product}`,
        html: `<p>${session.customer_details.email} bought ${session.metadata.product} for $${session.amount_total / 100}</p>`,
      }),
    });
  }

  res.json({ received: true });
}
Enter fullscreen mode Exit fullscreen mode

I used the Stripe SDK here because signature verification with raw crypto is error-prone. Worth the dependency for one function.

File 3: api/session.js

The success page loads with a session_id query param. This function fetches the session so I can show the right download links.

// api/session.js
export default async function handler(req, res) {
  const { session_id } = req.query;

  const response = await fetch(
    `https://api.stripe.com/v1/checkout/sessions/${session_id}`,
    { headers: { 'Authorization': `Bearer ${process.env.STRIPE_SECRET_KEY}` } }
  );

  const session = await response.json();

  // Generate signed download URL from Supabase Storage
  const product = session.metadata.product;
  const { data } = await supabase.storage
    .from('kit-downloads')
    .createSignedUrl(`${product}.zip`, 604800); // 7-day expiry

  res.json({
    product: session.metadata.product,
    email: session.customer_details.email,
    download_url: data.signedUrl,
  });
}
Enter fullscreen mode Exit fullscreen mode

Downloads live in a private Supabase Storage bucket. Signed URLs expire after 7 days. The success page tells customers to bookmark it.

The Cost

Service Monthly Cost
Vercel (hosting + serverless) $0
Stripe (per transaction only) $0
Resend (email notifications) $0
Supabase (download storage) $0
Total $0/month

Stripe takes 2.9% + $0.30 per transaction. Everything else is free tier.

What I'd Do Differently

1. Use the Stripe SDK for checkout too. I went with raw fetch to avoid dependencies but the URL-encoded params are annoying to maintain. The SDK is worth it.

2. Add an orders table. Stripe is technically the source of truth, but having a local record makes it easier to debug "I didn't get my download" emails.

3. Set up error monitoring from day one. I had a silent webhook failure for 2 days before I noticed. Resend was returning 422 because I hadn't verified my sending domain.

4. Consider Lemon Squeezy. If you want to sell digital products and don't care about custom checkout flows, Lemon Squeezy handles tax, delivery, and licensing out of the box. I went with Stripe because I wanted full control, but for most people Lemon Squeezy is less work.

The Takeaway

You don't need a backend framework to sell digital products. Three serverless functions handle the entire flow: create session, verify payment, deliver download.

The hard part isn't the code — it's the Stripe Dashboard setup (products, prices, webhook endpoints, tax settings) and testing the full flow end-to-end before going live.


This post was originally published on dashbuilds.dev.

I sell deploy-ready developer kits at dashbuilds.dev/shop — the checkout flow from this post is what powers it.

Top comments (0)