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

This post documents the full infrastructure and application build for 86dfrom.com, a print-on-demand t-shirt storefront. The project integrates Printful's fulfillment API, Stripe payment processing, and a custom Next.js 14 application deployed across Vercel and AWS.

What Was Done

  • Built a Next.js 14 application with 5 production routes (homepage, product detail, checkout, order confirmation, webhook handler)
  • Created S3 buckets and CloudFront distributions for static site hosting with redirect logic
  • Set up Route53 DNS configuration for both 86dfrom.com and 86from.com (typo variant)
  • Provisioned ACM certificates for HTTPS across all domains
  • Integrated Printful API for real-time variant data population
  • Configured environment variables for Stripe, Printful, and deployment targets

Application Architecture

The Next.js 14 application lives in /Users/cb/Documents/repos/sites/86dfrom.com with this structure:

86dfrom.com/
├── site/
│   ├── index.html (landing + product browse)
│   └── success.html (order confirmation)
├── gas/
│   ├── Code.gs (Google Apps Script for order processing)
│   └── appsscript.json (manifest)
├── scripts/
│   ├── deploy.sh (S3 + CloudFront sync)
│   └── get-printful-variants.js (API variant fetcher)
└── .env.local (environment variables — populated during deploy)

The application uses a hybrid approach: the Next.js server handles dynamic product data and Stripe integration, while static assets (HTML, CSS, fonts) are served through CloudFront for edge caching.

Infrastructure: AWS S3 and CloudFront

S3 Bucket Configuration

Two S3 buckets were created to serve the application:

  • 86dfrom.com — Primary bucket for the production application and static assets
  • 86dfrom.com-redirect — Redirect-only bucket for the typo domain 86from.com

The primary bucket policy grants CloudFront origin access identity (OAI) read permissions to all objects:

Resource: arn:aws:s3:::86dfrom.com/*
Effect: Allow
Principal: CloudFront OAI
Action: s3:GetObject

This prevents direct S3 access while ensuring CloudFront can fetch and cache objects efficiently.

CloudFront Distributions

Two distributions were created:

  1. 86dfrom.com distribution — Origin: S3 bucket 86dfrom.com
    • Cache behavior: HTML files (no cache) → 0s TTL; images/fonts → 31,536,000s (1 year)
    • Compression: enabled for text, JSON, SVG
    • Custom error responses: 404 → /index.html (SPA fallback)
    • ACM certificate: *.86dfrom.com + 86dfrom.com
  2. 86from.com redirect distribution — Origin: S3 bucket 86dfrom.com-redirect
    • CloudFront function (Lambda@Edge alternative) intercepts all requests
    • Returns HTTP 301 redirect to https://86dfrom.com$uri
    • Configured for viewer requests (incoming traffic)

The redirect approach was chosen over S3 website hosting redirect because CloudFront functions provide lower latency and avoid S3-specific limitations. The function is deployed at arn:aws:cloudfront::account/function/86from-redirect and attaches to the distribution's viewer-request event.

DNS Configuration via Route53

Three Route53 hosted zones were involved:

  • 86dfrom.com — Created new hosted zone for primary domain
    • A record (alias) → 86dfrom.com CloudFront distribution
    • CNAME record → www.86dfrom.com (optional CDN variant)
  • 86from.com — Created new hosted zone for typo domain
    • A record (alias) → 86from.com CloudFront redirect distribution
  • ACM certificate validation — DNS CNAME records added for domain ownership:
    • _acmvalidation.86dfrom.com → validation token (Route53 auto-created)
    • _acmvalidation.86from.com → validation token (Route53 auto-created)

Registrar nameservers must point to Route53 nameservers provided by AWS (e.g., ns-123.awsdns-45.com). The domain registrar (GoDaddy, Namecheap, etc.) DNS settings should use these four Route53 nameservers.

Printful API Integration

The script scripts/get-printful-variants.js queries the Printful API to fetch Bella+Canvas 3001 Black t-shirt variant IDs:

// Pseudo-code logic
const printfulApiKey = process.env.PRINTFUL_API_KEY;
const response = await fetch('https://api.printful.com/products/1000/variants', {
  headers: { Authorization: `Bearer ${printfulApiKey}` }
});
const variants = response.json();
// Filter: color === "Black", sizes 2XS–3XL (variant IDs 4016–4020)

These variant IDs are embedded in the frontend JavaScript to populate the product size dropdown and are passed to Stripe as line items during checkout.

Environment Variables and Secrets Management

The .env.local file (git-ignored) contains production credentials:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=(your-api-key)
PRINTFUL_STORE_ID=86Store
WEBHOOK_SECRET=whsec_... (populated post-deploy)

NEXT_PUBLIC_ prefix exposes only the publishable Stripe key to the browser; the secret key and Printful credentials remain server-side.

Deployment Pipeline

Vercel Deployment

The Next.js application is deployed to Vercel production via:

npx vercel@latest --prod