```html

Building a Printful-Integrated T-Shirt Storefront: Next.js 14, Stripe, and AWS Infrastructure

This post covers the complete setup of 86dfrom.com, a print-on-demand t-shirt e-commerce site built with Next.js 14, integrated with Printful for inventory management, Stripe for payments, and deployed across Vercel + AWS CloudFront/S3. We'll walk through the architecture decisions, infrastructure setup, and deployment pipeline.

Project Architecture Overview

The site is structured as a monorepo with three distinct deployment targets:

  • Next.js frontend + API routes → Vercel (primary application)
  • Static HTML fallback → S3 + CloudFront (disaster recovery, CDN caching)
  • Google Apps Script webhooks → GAS deployment (order notifications)

This separation allows us to handle dynamic commerce logic (Stripe webhooks, Printful sync) via Vercel's serverless functions while maintaining a static CDN-backed fallback for high-traffic scenarios.

File Structure and Source Organization

The project lives in two locations during development and production:

~/Desktop/86dfrom/                    # Development workspace
├── site/
│   ├── index.html                    # Main product page
│   └── success.html                  # Post-checkout confirmation
├── gas/
│   ├── Code.gs                       # Google Apps Script backend
│   └── appsscript.json               # GAS manifest (scopes, version)
├── scripts/
│   ├── deploy.sh                     # S3 + CloudFront invalidation
│   └── get-printful-variants.js      # Variant ID fetcher
└── .env.local                        # Secrets (Stripe, Printful)

~/Documents/repos/sites/86dfrom.com/  # Git source of truth
└── [mirror of above]

Both directories are synced via cp -r during the dev session; the repo directory becomes the canonical source for version control.

Printful Integration and Variant Management

Printful hosts the actual inventory and fulfillment. The site needs to reference specific product variants (SKUs) to process orders correctly.

Variant IDs used: 4016, 4017, 4018, 4019, 4020 (Bella+Canvas 3001 Black, various sizes). These are fetched via:

node scripts/get-printful-variants.js

The script calls the Printful API endpoint with an API token stored in .env.local under NEXT_PUBLIC_PRINTFUL_API_KEY, filters by product name and color, and outputs variant objects containing:

  • Variant ID (used in order creation)
  • Size
  • Price
  • Available inventory status

These values populate the frontend dropdown and are passed to the Stripe checkout session metadata, then to Printful's order creation endpoint when webhooks fire.

Environment Configuration Strategy

The .env.local file contains three categories of credentials:

  • Printful API key — Used by scripts/get-printful-variants.js and GAS Code.gs for order sync
  • Stripe secret key — Used by /api/webhook to verify webhook signatures and create orders
  • Stripe publishable key — Embedded in frontend HTML for client-side Checkout initialization

This file is .gitignore'd in the repo but must be manually created before each deployment. For Vercel deployments, these same values are added to the project settings at vercel.com → 86dfrom → Settings → Environment Variables.

Infrastructure: S3 + CloudFront + Route53

S3 Bucket Setup

Created S3 bucket 86dfrom.com (region: us-east-1) with:

  • Static website hosting enabled (index: index.html)
  • Bucket policy allowing CloudFront OAI (Origin Access Identity) to read all objects
  • CORS headers configured for font and image assets
# Deploy via:
aws s3 sync ~/Documents/repos/sites/86dfrom.com/site/ \
  s3://86dfrom.com --delete

# Invalidate CloudFront cache:
aws cloudfront create-invalidation \
  --distribution-id E1A2B3C4D5E6F7 \
  --paths "/*"

CloudFront Distributions

Primary distribution (86dfrom.com):

  • Origin: S3 bucket with OAI (not public website endpoint)
  • Default root object: index.html
  • Cache behaviors:
    • Static assets (CSS, JS, fonts): 30-day TTL
    • HTML: 5-minute TTL (allows quick fixes without full invalidation)
  • Viewer protocol: Redirect HTTP → HTTPS
  • Custom domain: 86dfrom.com
  • SSL certificate: ACM-managed, auto-renewal

Redirect distribution (86from.com):

A second distribution handles the typo domain (86from.com without "d"). It uses a CloudFront function (Lambda@Edge alternative) to 301-redirect all traffic to the primary domain. This prevents user confusion and consolidates SEO equity.

// Redirect function deployed to 86from.com distribution
function handler(event) {
  var request = event.request;
  var host = request.headers.host.value;
  
  if (host === '86from.com') {
    return {
      statusCode: 301,
      statusDescription: 'Moved Permanently',
      headers: {
        'location': { value: 'https://86dfrom.com' + request.uri }
      }
    };
  }
  return request;
}

Route53 DNS Records

Two hosted zones were configured:

  • 86dfrom.com zone:
    • A record → CloudFront distribution alias (primary site)
    • AAAA record → CloudFront IPv6 alias
    • CNAME for ACM certificate validation (temporary, auto-removed post-validation)
  • 86from.com zone:
    • A record → Redirect CloudFront distribution alias
    • AAAA record → Redirect CloudFront IPv6 alias

Vercel Deployment and API Routes

The Next.js application is deployed to