```html

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

This post documents the complete infrastructure and application setup for 86dfrom.com, a print-on-demand t-shirt storefront built with Next.js 14, integrated with Printful's API for inventory management, Stripe for payments, and deployed across Vercel (frontend) and AWS (static assets). We'll cover the architectural decisions, deployment patterns, and the specific tooling that ties it all together.

Project Architecture Overview

The 86dfrom project follows a hybrid deployment model:

  • Frontend: Next.js 14 application deployed to Vercel (handles SSR, API routes, and dynamic content)
  • Static Assets: S3 + CloudFront CDN for font caching and high-availability image delivery
  • DNS: Route53 for domain management and traffic routing
  • Payment Processing: Stripe integration with webhook handlers at /api/webhook
  • Print Fulfillment: Printful API for real-time variant data and order routing

The application structure mirrors this separation:

/site                    # Next.js frontend (vercel deployment)
/gas                     # Google Apps Script (future webhook processor)
/scripts                 # Deployment and integration scripts
  └─ deploy.sh          # S3 + CloudFront invalidation
  └─ get-printful-variants.js  # Fetch product variant IDs from Printful API
.env.local              # Secrets (Stripe keys, Printful API token)

Printful Integration: Variant Population Strategy

The Printful integration required careful product configuration. Rather than hard-coding variant IDs, we implemented a discovery pattern using the Printful API.

The file /scripts/get-printful-variants.js queries the Printful API endpoint GET /v2/stores/{store_id}/products and filters for the Bella+Canvas 3001 black t-shirt (unisex fit). This script outputs five variant IDs corresponding to sizes XS through XL:

  • 4016 – XS
  • 4017 – S
  • 4018 – M
  • 4019 – L
  • 4020 – XL

These IDs are then embedded in .env.local as:

NEXT_PUBLIC_PRINTFUL_VARIANT_IDS=4016,4017,4018,4019,4020

This approach allows size selection on the frontend without additional API calls; the variant ID maps directly to Printful's fulfillment system.

Static Asset Delivery: S3 + CloudFront Configuration

To reduce latency for fonts and images, we provisioned an S3 bucket and CloudFront distribution separate from the Vercel deployment.

S3 Bucket Configuration:

  • Bucket name: 86dfrom-site-assets
  • Region: us-east-1 (CloudFront origin requirement)
  • Versioning: enabled for rollback capability
  • Bucket policy: restricts access to CloudFront origin identity only (OAI pattern)

CloudFront Distribution Setup:

  • Origin: S3 bucket with Origin Access Identity (OAI) to prevent direct bucket access
  • Distribution ID: E1ABCDEF123GHIJ (example; actual ID assigned on creation)
  • CNAME: cdn.86dfrom.com
  • Certificate: ACM certificate for 86dfrom.com (with wildcard support)
  • Default TTL: 31536000 seconds (1 year) for immutable assets like fonts
  • Cache behavior: gzip compression enabled for CSS/JS

The S3 bucket policy restricts all access to the CloudFront OAI:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity EXXXXX"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::86dfrom-site-assets/*"
    }
  ]
}

Font files (specifically Google's Anton font in woff2 format) are cached through this distribution rather than hitting Google's servers on each request.

DNS Configuration in Route53

The primary domain 86dfrom.com uses Route53 hosted zone for all DNS records:

  • 86dfrom.com A record → Vercel's managed DNS (alias to Vercel nameserver)
  • cdn.86dfrom.com CNAME → CloudFront distribution domain name
  • www.86dfrom.com CNAME → Vercel deployment (optional, Vercel handles this)

We also created a secondary distribution for the typo domain 86from.com (missing the 'd') with a CloudFront redirect function that issues a 301 permanent redirect to the correct domain:

// CloudFront function (viewer request) at 86from.com distribution
if (request.uri === '/') {
  return {
    statusCode: 301,
    statusDescription: 'Moved Permanently',
    headers: {
      'location': { value: 'https://86dfrom.com/' }
    }
  };
}

This function is published to the viewer-request event of the 86from.com CloudFront distribution, ensuring search engines update their index and users arrive at the correct domain.

Environment Configuration and Secrets Management

The .env.local file contains three critical integrations:

NEXT_PUBLIC_PRINTFUL_VARIANT_IDS=4016,4017,4018,4019,4020
STRIPE_SECRET_KEY=sk_live_... (or sk_test_... for staging)
STRIPE_WEBHOOK_SECRET=whsec_... (populated after Vercel webhook registration)
PRINTFUL_API_KEY=... (store token with full API scopes)

Why this split?

  • NEXT_PUBLIC_PRINTFUL_VARIANT_IDS is prefixed NEXT_PUBLIC_ because size selection is client-side; this value is safe to expose in the browser bundle
  • STRIPE_SECRET_KEY and PRINTFUL_API_KEY are server-only; they're used in /api routes and never sent to the client
  • STRIPE_WEBHOOK_SECRET is used exclusively in the webhook handler at /api/webhook to verify request signatures

In Vercel,