```html

Building a Print-on-Demand T-Shirt Site with Next.js 14, Printful, and Stripe: Infrastructure & Deployment Strategy

Overview

We built a full-stack e-commerce platform for 86dfrom.com — a print-on-demand t-shirt storefront powered by Next.js 14, integrated with Printful's API for inventory management, and Stripe for payment processing. This post details the architecture decisions, deployment pipeline, and infrastructure setup that enables seamless order fulfillment.

Project Structure & Build Setup

The project lives at /Users/cb/Documents/repos/sites/86dfrom.com with a clean Next.js 14 foundation:

  • /site — Frontend HTML (static assets, landing page, success confirmation)
  • /gas — Google Apps Script integration (webhook processing, data logging)
  • /scripts — Deployment automation and data fetching utilities
  • .env.local — Environment configuration (Stripe keys, Printful token, webhook secrets)
  • appsscript.json — Google Apps Script manifest with OAuth scopes

The Next.js build compiles all 5 API routes cleanly with zero errors:

npm run build
# Output: compiled successfully ✓

This validates that route handlers, middleware, and dependencies are correctly wired before deployment.

Printful Integration: API Architecture

We use Printful as the single source of truth for product inventory and variant management. The integration flow:

  • Variant Discoveryscripts/get-printful-variants.js fetches the Bella+Canvas 3001 Black t-shirt (product ID from store) and extracts all size/color variants, writing their IDs to .env.local
  • Inventory Lookup — API route /api/variants returns available sizes and pricing by reading stored variant IDs
  • Order Creation/api/create-order accepts a variant ID + size selection, calculates Stripe charge, then submits fulfillment order to Printful

The key decision: store variant IDs in env rather than querying Printful on every request. This reduces API calls, improves latency, and lets us version control product catalogs. Variant IDs are immutable within a store, so a one-time fetch is sufficient.

Example variant ID storage in .env.local:

NEXT_PUBLIC_PRINTFUL_VARIANT_4016=4016
NEXT_PUBLIC_PRINTFUL_VARIANT_4017=4017
NEXT_PUBLIC_PRINTFUL_VARIANT_4018=4018
NEXT_PUBLIC_PRINTFUL_VARIANT_4019=4019
NEXT_PUBLIC_PRINTFUL_VARIANT_4020=4020
PRINTFUL_API_KEY=[redacted]

Payment Processing: Stripe Webhook Architecture

Stripe handles payment collection and fraud detection. Our webhook flow:

  1. Customer submits order via /api/create-order, which calls stripe.paymentIntents.create()
  2. Stripe returns a payment intent and client secret to the frontend
  3. Frontend uses Stripe.js to confirm payment (3D Secure, etc.)
  4. On success, webhook at /api/webhook receives payment_intent.succeeded event
  5. Webhook verifies signature using stored STRIPE_WEBHOOK_SECRET
  6. On verification, we create Printful fulfillment order and log to Google Sheets

This architecture decouples payment confirmation from order fulfillment, allowing retries and audit trails. The webhook secret is unique per environment (test vs. live), so we store separate keys in Vercel.

Infrastructure: DNS, CDN, and Static Hosting

S3 + CloudFront Setup

We deployed static assets to S3 and served via CloudFront for global caching:

  • S3 Bucket: 86dfrom-site-production (region: us-east-1)
  • CloudFront Distribution ID: (retrieved via AWS CLI; cached in project docs)
  • ACM Certificate: Wildcard cert for *.86dfrom.com (issued, DNS validated in Route53)
  • Origin: S3 bucket with origin access identity (OAI) restricting public reads

CloudFront configuration enforces HTTPS, compresses text assets, and invalidates cache on deploy via:

aws cloudfront create-invalidation --distribution-id [ID] --paths "/*"

Route53 DNS Records

Domain registrar holds authoritative NS records pointing to Route53 hosted zone:

  • A record: 86dfrom.com → CloudFront distribution alias
  • CNAME: www.86dfrom.com → CloudFront distribution
  • CNAME: 86from.com → separate CloudFront redirect distribution (typo-squatting protection)

ACM certificate validation records were added as CNAMEs and validated automatically by AWS.

Redirect Distribution (86from.com)

We created a secondary CloudFront distribution with a CloudFront Function that redirects 86from.com/* to 86dfrom.com/*. This is deployed via scripts/deploy.sh, which:

#!/bin/bash
# Create/update CloudFront redirect function
aws cloudfront create-function \
  --name 86from-redirect \
  --auto-publish \
  --function-code fileb://redirect-function.js

# Deploy to redirect distribution
aws cloudfront create-distribution-with-tags \
  --distribution-config-with-tags [config]

This pattern catches traffic to the typo domain and funnels users to the correct site without requiring a separate S3 bucket.

Vercel Deployment Pipeline

The Next.js app (dynamic routes: /api/*) is deployed to Vercel production via:

npx vercel@latest --prod

Environment variables are set post-deploy:

  • STRIPE_SECRET_KEY — Live or test secret key
  • STRIPE_WEBHOOK_SECRET — Endpoint secret from webhook configuration
  • PRINTFUL_API_KEY — Store token with all scopes
  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY — Public key for frontend
  • Variant ID env vars (prefixed NEXT_PUBLIC_PRINTFUL_VARIANT_*)

Vercel's deployment creates a production URL (e.g., 86dfrom.vercel.app initially), which becomes the API backend. Route53 CNAME records point api.86dfrom.com to this Vercel domain,