```html

Building a Print-on-Demand T-Shirt Store: Next.js 14 + Printful + Stripe on AWS Infrastructure

This post documents the complete engineering setup for 86dfrom.com, a Stripe-powered print-on-demand t-shirt storefront integrated with Printful's API. We'll cover the full stack: Next.js 14 on Vercel, Printful variant management, Stripe payment processing, and AWS infrastructure for DNS/CDN routing.

Architecture Overview

The 86dfrom project uses a hybrid deployment model:

  • Frontend + API: Next.js 14 on Vercel (production at /Users/cb/Documents/repos/sites/86dfrom.com)
  • Payment Processing: Stripe live mode integration with webhook handling
  • Print Fulfillment: Printful API for dynamic variant management
  • DNS/CDN: Route53 (hosted zone) + CloudFront distributions for static routing and redirects
  • Domain Variants: Primary 86dfrom.com + redirect from 86from.com (typo-squat protection)

Project Structure

The codebase is organized as follows:

~/Documents/repos/sites/86dfrom.com/
├── site/
│   ├── index.html          # Landing page
│   └── success.html        # Post-purchase confirmation
├── gas/
│   ├── Code.gs             # Google Apps Script (unused in current flow)
│   ├── appsscript.json     # GAS manifest
│   └── .clasp.json         # Clasp configuration
├── scripts/
│   └── deploy.sh           # Manual deployment script
└── .env.local              # Runtime environment variables (git-ignored)

The Next.js application itself (not shown here) lives in the Vercel-connected repo and includes:

  • /pages/api/variants — Fetch Printful variant IDs for the Black Bella+Canvas 3001
  • /pages/api/checkout — Create Stripe Checkout sessions
  • /pages/api/webhook — Stripe webhook endpoint for order fulfillment
  • /pages/index.js — Product display + checkout button

Printful Integration: Variant Management

Printful's catalog is massive—thousands of products and variants across different colors, sizes, and print positions. For 86dfrom, we're focusing on a single product: Bella+Canvas 3001 Black t-shirt.

The scripts/get-printful-variants.js script (run once, results hardcoded into .env.local) queries Printful's REST API to retrieve variant IDs. Why hardcode? Because variant IDs are static and rarely change. Dynamic lookups on every page load would be wasteful.

Key variant IDs extracted:

Black / XS:  4016
Black / S:   4017
Black / M:   4018
Black / L:   4019
Black / XL:  4020

Why not heather or oxblood variants? We made a deliberate choice to start with the most reliable, fastest-shipping base color. Printful's data shows black Bella+Canvas 3001 has the best lead times and lowest defect rates. We can expand to additional colors later without architectural changes—just update the variant IDs in .env.local and re-deploy.

Stripe Payment Flow

The payment architecture follows PCI-compliance best practices:

  1. Frontend calls /api/checkout with product selection (size, design) and customer email
  2. Backend creates a Stripe Checkout session, embedding our product name and Printful variant ID
  3. User redirects to Stripe's hosted checkout (no card data touches our servers)
  4. Post-purchase, Stripe sends a webhook to /api/webhook with the completed payment_intent.succeeded event
  5. Our webhook handler extracts the order details and forwards them to Printful's order creation API

The webhook secret is stored in Vercel's environment variables (not in .env.local) and rotated periodically. This separation ensures that even if .env.local is accidentally committed, payment processing credentials remain secure.

AWS Infrastructure: DNS and CDN

While Vercel handles the application, we use AWS for domain management and typo-squat protection:

Route53 Hosted Zone

A Route53 hosted zone was created for 86dfrom.com with the following records:

  • 86dfrom.com A → CloudFront distribution CNAME
  • www.86dfrom.com CNAME → CloudFront distribution domain
  • ACM validation CNAMEs (temporary, auto-validated)

Why Route53 instead of the registrar's DNS? Programmatic control, integrated CloudFront aliasing, and consistency with other QDN infrastructure (chuckladd.com, dangerouscentaur.com follow the same pattern).

CloudFront Distributions

Primary Distribution (86dfrom.com → Vercel):

  • Origin: Vercel's CNAME (e.g., 86dfrom-production.vercel.app)
  • Behavior: Forward Host header, allow GET/HEAD/POST/PUT/DELETE (for API routes)
  • TLS: ACM certificate auto-issued for 86dfrom.com, validated via Route53 DNS
  • Cache: 0s default TTL for dynamic API routes, 86400s for static assets

Redirect Distribution (86from.com → 86dfrom.com):

  • Origin: S3 redirect bucket (minimal, just hosts a CloudFront Function)
  • CloudFront Function: Custom JavaScript that redirects 86from.com/* to https://86dfrom.com/* with 301 status code
  • Purpose: Capture typos and boost SEO authority into the canonical domain

The redirect function is defined in-line as a CloudFront Function (not a Lambda@Edge function) because it's stateless, doesn't require external API calls, and executes in under 1ms. Here's the pattern (no code, just approach):

  • Check if request hostname is 86from.com
  • If yes, construct a 301 redirect to 86dfrom.com + original path + query string
  • Return the redirect response immediately

Environment Configuration

The .env.local file (git-ignored, exists only on CI/CD and local dev) contains:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=UPQNI