```html

Building a Printful-Integrated T-Shirt Store on Next.js 14: Infrastructure, DNS, and Deployment Pipeline

This post documents the complete buildout of 86dfrom.com, a Printful-integrated e-commerce site for custom t-shirt sales. We built a full Next.js 14 application with Stripe payment processing, Google Apps Script backend integration, and a multi-region cloud infrastructure spanning AWS S3, CloudFront, Route53, and Vercel. Here's what went into it.

Architecture Overview

The site uses a hybrid deployment model:

  • Frontend: Next.js 14 app deployed to Vercel (production environment)
  • Backend: Google Apps Script (GAS) for email/notification workflows
  • Payment processing: Stripe for transactions; Printful API for order fulfillment
  • Static hosting: AWS S3 + CloudFront for legacy content and redirects
  • DNS: Route53 for all domain routing and health checks

This architecture was chosen to separate concerns: Vercel handles the dynamic Next.js application with built-in edge caching and automatic deployments, while S3/CloudFront handles static content and subdomain redirects at the edge. Google Apps Script provides a serverless, no-ops email workflow that's already integrated with Google Workspace.

Project Structure and File Organization

The project lives in /Users/cb/Documents/repos/sites/86dfrom.com/ with this layout:

86dfrom.com/
├── site/                    # Static HTML (hosted on S3/CloudFront)
│   ├── index.html
│   └── success.html
├── gas/                     # Google Apps Script backend
│   ├── Code.gs
│   ├── appsscript.json
│   └── .clasp.json
├── scripts/
│   ├── deploy.sh            # S3 + CloudFront deployment
│   └── get-printful-variants.js
├── .env.local               # Runtime secrets (Stripe, Printful keys)
└── [Next.js app files]      # app/, pages/, lib/, etc.

This split exists because we're running both a Vercel-deployed Next.js app and a static redirect site on 86from.com (note: different domain). The static site handles edge redirects before traffic even hits our origin.

Infrastructure: AWS and DNS Setup

S3 Buckets and CloudFront Distributions

We created two S3 buckets and corresponding CloudFront distributions:

  • 86dfrom.com bucket: Hosts the Next.js static exports and serves as origin for the main site CloudFront distribution
  • 86from.com bucket: Hosts a simple redirect HTML file; CloudFront distribution includes a Lambda@Edge function to rewrite traffic

The bucket policies were configured to allow CloudFront's origin access identity (OAI) to read objects, blocking direct S3 URL access:

aws s3api put-bucket-policy \
  --bucket 86dfrom.com \
  --policy file://bucket-policy.json

where the policy grants s3:GetObject only to the CloudFront distribution's OAI ARN.

ACM Certificates and HTTPS

AWS Certificate Manager certificates were requested for both domains:

aws acm request-certificate \
  --domain-name 86dfrom.com \
  --subject-alternative-names www.86dfrom.com \
  --validation-method DNS

DNS validation required adding CNAME records to Route53 hosted zone. The validation records were extracted from ACM and added:

aws route53 change-resource-record-sets \
  --hosted-zone-id [ZONE_ID] \
  --change-batch file://dns-validation.json

We waited for validation (typically 5–15 minutes) before proceeding with CloudFront distribution creation.

CloudFront Distributions and Lambda@Edge

The main 86dfrom.com CloudFront distribution was created with:

  • Origin: S3 bucket with OAI
  • Viewer protocol policy: Redirect HTTP to HTTPS
  • Default cache behavior: Cache-Control headers from origin, 1-day TTL
  • Alternate domain names: 86dfrom.com, www.86dfrom.com
  • ACM certificate: The validated cert from above

The 86from.com distribution (redirect domain) includes a CloudFront Function (not Lambda@Edge, due to simplicity) that rewrites all requests:

// CloudFront Function for 86from.com redirect
function handler(event) {
  var request = event.request;
  var headers = request.headers;
  
  var newurl = 'https://86dfrom.com' + request.uri;
  if (request.querystring) {
    newurl += '?' + request.querystring;
  }
  
  return {
    statusCode: 301,
    statusDescription: 'Moved Permanently',
    headers: {
      'location': { value: newurl }
    }
  };
}

This function was published and associated with the 86from.com CloudFront viewer request event.

Route53 DNS Records

Final DNS configuration in Route53 for hosted zone 86dfrom.com:

86dfrom.com.        A       [CloudFront dist alias, e.g., d123abc.cloudfront.net]
www.86dfrom.com.    CNAME   [Same CloudFront dist]
86from.com.         A       [Redirect CloudFront dist alias]
_acmvalidation.86dfrom.com. CNAME [ACM validation record]

Route53 alias records were used (not CNAME) for root domain 86dfrom.com pointing to CloudFront, which is an AWS best practice for apex domains.

Next.js Application Setup

The Next.js 14 app at /Users/cb/Documents/repos/sites/86dfrom.com/ includes five API routes:

  • app/api/variants.ts — Fetches Printful variant IDs and pricing from .env.local
  • app/api/create-checkout.ts — Creates Stripe checkout sessions
  • app/api/webhook.ts — Stripe webhook handler for order confirmation
  • app/api/submit-order.ts — Submits orders to Printful API
  • app/api/email.ts — Triggers Google Apps Script email notifications

All routes handle CORS, validate Stripe signatures, and log to stdout for Vercel's edge runtime. Environment variables required:

PRINTFUL_API_KEY=UPQNIqzJkpoV2JPKKrhwYteCKzhipRnLHA2TxLnt
STRIPE_SECRET_KEY=sk_live_[