Next.js Ecommerce
How to Build a Cart and Stripe Checkout with Next.js
Implement anonymous and customer carts, server-owned prices, Stripe Checkout, signed webhooks, idempotent orders, and inventory validation in Next.js.
A cart records purchase intent. It does not authorize a price, reserve stock automatically, or prove that payment completed.
Choose cart identity and lifetime
Create a random cart token for anonymous shoppers and store it in a secure HTTP-only cookie. The database owns the cart, line variant IDs, quantities, timestamps, and status. After login, associate or merge it with the customer according to explicit rules.
Set an inactivity expiry and decide whether checked-out carts become immutable snapshots. Do not store the entire cart or authoritative prices in a browser-readable cookie where customers and extensions can change them.
Validate every cart mutation
An add action accepts a variant ID and quantity, resolves the cart from the server-controlled token, verifies that the variant is active, applies quantity limits, and upserts the line. The action never accepts account ownership, SKU price, or currency from the browser.
Return an expected error for invalid or unavailable variants. Revalidate the cart display after the write so the user reads their own update. Authorization and ownership checks belong inside the action even when the UI only exposes valid controls.
"use server";
export async function addToCart(variantId: string, quantity: number) {
const cart = await requireCart();
const variant = await getPurchasableVariant(variantId);
if (!variant) return { error: "Variant unavailable" };
await upsertCartLine(cart.id, variant.id, clampQuantity(quantity));
updateTag("cart:" + cart.id);
}Merge anonymous and customer carts
On sign-in, load both carts in one server transaction. Combine identical variants up to purchase limits, keep valid unique lines, and report removed or changed items. Mark the anonymous cart merged so replaying the same request cannot duplicate quantities.
Do not silently choose the most expensive or newest cart. The product should define whether the customer cart wins, the carts merge, or the shopper chooses. Test the behavior on multiple tabs and repeated login callbacks.
Reprice before Checkout
Load every active variant and current price on the server, apply validated discounts, calculate tax inputs and shipping choices, then create a pending-order snapshot. If a price or availability changed, return the updated cart for explicit confirmation instead of charging a different amount silently.
Build Stripe line items from server records. Add the pending-order ID as metadata and use a stable idempotency key for a repeated checkout attempt where the business wants one provider operation.
Confirm payment through webhooks
The success URL shows that Checkout returned; it does not prove settlement. Verify the webhook signature against the raw request body, locate the pending order from trusted metadata, and record the Stripe event under a unique constraint.
Create or mark the final order once, then queue confirmation email and fulfillment. A duplicate event should return success without repeating those effects. An expired session can release reservations and return the cart to an editable state.
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get("stripe-signature");
const event = stripe.webhooks.constructEvent(
rawBody,
signature,
process.env.STRIPE_WEBHOOK_SECRET,
);
await processCheckoutEventOnce(event);
return new Response(null, { status: 200 });
}Protect cart and order reads
Anonymous cart access requires the unguessable server-issued token. Customer carts and orders require the authenticated owner. Order confirmation pages should load by both the public-safe reference and owner context rather than exposing sequential database IDs.
Return narrow DTOs to Client Components. Payment provider objects, internal fraud fields, webhook payloads, and database errors remain on the server. Logs should use order and event identifiers without printing payment or address data unnecessarily.
Test tampering, retries, and expiry
Change the submitted variant, quantity, and displayed price; replay add and merge actions; deliver a webhook twice; deliver events out of order; expire Checkout; remove stock during payment; and request another customer's order. Every outcome should be safe and explainable.
Deploy the production build on Adios with Stripe and database secret references. Verify the generated HTTPS route in test mode, inspect runtime logs, and register the stable custom-domain webhook only after the release reports healthy.