```html

Building a Printful + Stripe T-Shirt Store on Next.js 14: Infrastructure, API Integration, and Deployment Strategy

This post walks through the complete build of 86dfrom.com, a Next.js 14 e-commerce site powered by Printful for print-on-demand fulfillment and Stripe for payments. We'll cover the architecture decisions, infrastructure setup across AWS/CloudFront, and the integration patterns that make this stack work.

Project Structure & Architecture

The project lives at /Users/cb/Documents/repos/sites/86dfrom.com with this layout:

86dfrom.com/
├── site/               # Static assets + HTML shell
├── gas/                # Google Apps Script (optional backend)
├── scripts/            # Deployment automation
└── package.json        # Next.js 14 app root

The why here: we're treating this as a hybrid deployment—the Next.js app lives on Vercel (for dynamic routes and serverless functions), while static assets can optionally be cached on CloudFront/S3. The gas/ directory exists for potential future webhook handling via Google Sheets or Apps Script integration, though Vercel's /api/webhook routes handle webhooks natively.

Printful Integration: Variant ID Discovery

The first blocker was populating variant IDs for the Bella+Canvas 3001 Black t-shirt. Printful's API requires exact variant IDs when creating orders; you can't ship without them.

The discovery process:

  1. Authenticate to Printful API with store credentials
  2. Fetch the product catalog for store 86Store
  3. Filter for Bella+Canvas 3001 in black colorway
  4. Extract the 5 size variants (XS, S, M, L, XL) and their numeric IDs

We created scripts/get-printful-variants.js to automate this. The script makes a GET request to:

GET https://api.printful.com/store/{store_id}/products
Authorization: Bearer {PRINTFUL_API_KEY}

This returns the full product tree. We parse it to find the specific SKU and extract variant IDs like 4016, 4017, 4018, 4019, 4020 for XS–XL in black.

Key decision: We ignored Heather and Oxblood colorways at this stage. Starting with one color reduces complexity; adding more colors is a data config change, not a code change.

Environment Configuration & Secrets Management

Once variant IDs were confirmed, we created .env.local at the project root with:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=...
PRINTFUL_STORE_ID=86Store
PRODUCT_VARIANTS={
  "XS": 4016,
  "S": 4017,
  "M": 4018,
  "L": 4019,
  "XL": 4020
}

Why split public/secret keys:

  • NEXT_PUBLIC_* variables are baked into client-side bundles—safe to expose Stripe's publishable key
  • STRIPE_SECRET_KEY stays server-side only, used in /api/checkout and /api/webhook routes
  • PRINTFUL_API_KEY is also server-side; we never send it to the browser

The variant mapping as JSON (or a TypeScript constant) lets us define sizes and IDs once, then reference them in both the product listing (site/index.html or a React component) and checkout logic.

Next.js 14 Build & Deployment Pipeline

The build chain is straightforward:

npm run build          # Compiles Next.js, outputs .next/ folder
npx vercel@latest --prod  # Deploys to Vercel production

Before deploying, we verified that all 5 API routes compile cleanly:

  • /api/products — returns available t-shirt sizes and prices
  • /api/checkout — creates a Stripe checkout session
  • /api/webhook — listens for Stripe payment.intent.succeeded events
  • /api/order-status — checks Printful order status by Stripe payment ID
  • /api/health — liveness check (optional, useful for monitoring)

Each route validates its request, logs to stdout, and returns JSON. No async issues; all database calls (if any) would go through Stripe or Printful's APIs, not a local DB at this stage.

Stripe Webhook Setup

After Vercel deployment goes live at a public domain (e.g., https://86dfrom.com), the next step is registering the webhook endpoint in Stripe's Dashboard:

  1. Navigate to Developers → Webhooks
  2. Add endpoint: https://86dfrom.com/api/webhook
  3. Select events: payment_intent.succeeded, charge.refunded
  4. Copy the signing secret (whsec_...) and add it to Vercel's environment variables as STRIPE_WEBHOOK_SECRET

/api/webhook then verifies the request signature using this secret before processing:

const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(body, sig, secret);

This prevents replay attacks and ensures only legitimate Stripe events trigger order creation.

DNS & Domain Configuration

For 86dfrom.com, we need:

  • A/CNAME record pointing to Vercel's endpoints (Vercel provides exact DNS values after project creation)
  • Optional: CloudFront distribution in front of S3 for static asset caching (if we use the site/ directory)

If using CloudFront + S3:

  1. Create S3 bucket: 86dfrom-com-assets
  2. Request ACM certificate for 86dfrom.com (DNS validation)
  3. Add CNAME validation records to Route53 (if using Route53 for DNS)
  4. Create CloudFront distribution with S3 as origin, ACM cert attached
  5. Update Route53 to alias 86dfrom.com to CloudFront distribution

The scripts/deploy.sh` automates S3 upload and CloudFront cache invalidation:

#!/bin/bash
aws s3 sync site/ s3://86dfrom-com-assets --delete
aws cloudfront create-invalidation \