pealdevDOCS
Back to peal.dev

STRIPE STARTER · VALIDATION (ZOD)

Validation (Zod)

Every piece of data that enters the system from outside passes through a Zod schema in src/lib/validation.ts. Centralized, typed, and the open-redirect vectors are already plugged.

External input is anywhere a user, URL, or third party can influence the value: form data, search params, server action arguments, webhook headers. Every one of those goes through a schema in src/lib/validation.ts. Stripe webhook payloads are the only exception — they're verified by HMAC signature instead, because the raw body must be preserved.

What's already validated

TypeScript
// src/lib/validation.ts
stripeSessionIdSchema    // cs_test_xxx or cs_live_xxx
stripeCustomerIdSchema   // cus_xxx (min 8 chars)
stripePriceIdSchema      // price_xxx

checkoutInputSchema      // { priceId }
portalInputSchema        // { customerId, returnPath? }
successPageParamsSchema  // { session_id }

Using a schema (the safeParse pattern)

TypeScript
import { checkoutInputSchema } from "@/lib/validation";

// ALWAYS safeParse, never parse. Throwing belongs in tests, not user flows.
const result = checkoutInputSchema.safeParse({ priceId });

if (!result.success) {
  return { success: false, error: "Invalid price ID format" };
}

// result.data is now typed and trusted.
const { priceId: validPriceId } = result.data;

The open-redirect prevention pattern

The portal's returnPath is the kind of thing attackers love — pass https://evil.com and you've got an open redirect off your domain. Our schema requires it to start with / AND match a strict regex.

TypeScript
returnPath: z
  .string()
  .startsWith("/", "Return path must be a relative URL")
  .regex(/^\/[a-zA-Z0-9\-_/]*$/, "Return path contains invalid characters")
  .optional(),

Adding a new schema

  • Define it in src/lib/validation.ts (don't validate inline — centralize so the rules are auditable).
  • Export the inferred type: export type FooInput = z.infer<typeof fooInputSchema>.
  • Use safeParse at the trust boundary — Server Action entry, page param parse, webhook header check.
  • Add tests in src/lib/validation.test.ts. There are already 27 examples to copy from.

Never trust raw URL params or form data

It's tempting to do const id = searchParams.session_id ?? "" and call it a day. Don't. A malformed session ID hits stripe.checkout.sessions.retrieve(), Stripe throws, your error boundary catches a stack trace it didn't need. Pass everything through the right schema.

Stripe IDs are validated by PREFIX, not by existence

stripeCustomerIdSchema confirms the string starts with cus_, NOT that the customer exists in your Stripe account. That second check happens when you actually call the Stripe API. The schema catches typos and obvious attacks — the API call catches everything else.