Building a Printful-Integrated T-Shirt E-Commerce Site with Next.js 14, Stripe, and AWS CloudFront

We recently completed the infrastructure and initial application setup for 86dfrom.com, a print-on-demand t-shirt storefront built on Next.js 14 with Printful fulfillment and Stripe payments. This post details the technical architecture, deployment strategy, and integration patterns we used to ship a production-ready site in a single session.

What Was Built

The project is a modern e-commerce application that:

  • Serves static HTML/CSS/JS from CloudFront with S3 origin for the marketing site
  • Runs a Next.js 14 API backend (deployed to Vercel) for variant lookups, Stripe webhook handling, and Printful order integration
  • Fetches product variant IDs dynamically from Printful's API to display accurate inventory and pricing
  • Processes payments via Stripe with webhook validation
  • Manages DNS across two domains: 86dfrom.com (primary) and 86from.com (redirect) via Route53

The repository structure lives at ~/Documents/repos/sites/86dfrom.com/ with three key directories:

  • /site — static HTML/CSS served via CloudFront
  • /gas — Google Apps Script configuration (future webhook processing)
  • /scripts — deployment and utility scripts

Infrastructure Architecture

DNS & Domain Management (Route53)

We created separate hosted zones for both domain variations to ensure clean DNS records and flexibility for future subdomains:

  • Primary domain: 86dfrom.com — A record pointing to CloudFront distribution d12abc...cloudfront.net
  • Redirect domain: 86from.com — separate CloudFront distribution with HTTP redirect function, also via Route53

Both required ACM certificate validation. We added DNS CNAME records to Route53 hosted zones for automatic validation, eliminating manual email confirmation steps. Once validated, the certificates became immediately available for CloudFront attachment.

CDN & Static Asset Delivery (CloudFront + S3)

The static site is deployed to S3 bucket 86dfrom-site with the following architecture:

S3 Bucket (86dfrom-site)
  ├─ /site/index.html (main storefront)
  ├─ /site/success.html (post-purchase confirmation)
  └─ /assets/ (CSS, JS, images)
    ↓
CloudFront Distribution (d...cloudfront.net)
  ├─ Origin: S3 bucket
  ├─ Behaviors: cache HTML (300s TTL), assets (31536000s TTL)
  ├─ Function (Redirect): 301 for 86from.com traffic
  └─ SSL/TLS: ACM certificate for 86dfrom.com

Key caching decisions:

  • HTML files: 5-minute TTL (300 seconds) to allow rapid content updates without full invalidation
  • Static assets: 1-year TTL (31536000 seconds) with versioned filenames for cache busting
  • Invalidation: Post-deploy invalidation of /index.html and /success.html to ensure fresh content immediately

API & Backend (Vercel + Next.js 14)

The Next.js application handles dynamic logic with these routes:

  • /api/variants — fetches Printful product variant IDs and pricing
  • /api/checkout — initiates Stripe payment intents
  • /api/webhook — validates and processes Stripe webhook events (payment confirmations, disputes)
  • /api/order — submits orders to Printful after payment confirmation

The application structure:

next.js project root
├─ app/api/variants/route.js
├─ app/api/checkout/route.js
├─ app/api/webhook/route.js
├─ app/api/order/route.js
└─ .env.local (credentials, populated pre-deploy)

All routes were verified with a clean Next.js build before deployment:

$ npm run build
# Output: compiled successfully

Printful Integration

We use the Printful API (base URL: api.printful.com) with token-based authentication. The integration retrieves product variant IDs for Bella+Canvas 3001 Black t-shirts (5 variants, SKUs 4016–4020).

The variant fetching script (/scripts/get-printful-variants.js) runs once to populate the environment file with variant IDs. This script:

  1. Authenticates to Printful using the API token in .env.local
  2. Queries the /api/v2/products endpoint for the product ID
  3. Filters for Black colorway variants
  4. Extracts the 5 variant IDs
  5. Outputs them in format: VARIANT_IDS=id1,id2,id3,id4,id5

Why separate the variant fetch into a pre-deploy script? Printful variant IDs are stable during the product lifecycle but may change if inventory is restructured. Running this once during setup ensures our environment file is authoritative and reduces runtime API calls to Printful.

Stripe Payment Processing

Payments flow through Stripe with the following architecture:

  • Client-side: Frontend collects card details via Stripe Elements (secure, PCI-compliant)
  • API: /api/checkout creates a PaymentIntent, returns client secret to frontend
  • Confirmation: Frontend confirms payment with client secret
  • Webhook: /api/webhook listens for payment_intent.succeeded events, submits order to Printful

The webhook endpoint uses raw request body parsing to validate Stripe's signature before processing:

POST /api/webhook
Headers: stripe-signature: t=...,v1=...
Body: raw (unparsed) for signature verification

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

Deployment Strategy

Static Site to S3 + CloudFront

The deployment script (/scripts/deploy.sh) handles:

#!/bin/bash
# Sync local /site directory to S3
aws s3 sync ./site s3://86dfrom-site/site \
  --delete \
  --cache-control "max-age=300"

# Invalidate CloudFront to force refresh