Building a Multi-Domain T-Shirt Commerce Site with Next.js 14, Google Apps Script, and AWS CloudFront

This post documents the full-stack deployment of 86dfrom.com, a Printful-integrated t-shirt e-commerce site built with Next.js 14, backed by Google Apps Script for order processing, and fronted with AWS CloudFront + S3 for static hosting and CDN distribution. The project required coordinating multiple infrastructure layers: application deployment, serverless backend functions, DNS routing, SSL/TLS certificates, and CDN invalidation.

Architecture Overview

The final architecture consists of:

  • Primary application: Next.js 14 deployed to Vercel (production environment at /Users/cb/Documents/repos/sites/86dfrom.com)
  • Order backend: Google Apps Script deployed via Clasp, handling Stripe webhook ingestion and order routing to Printful API
  • Static hosting: AWS S3 bucket (86dfrom.com and 86from.com redirect bucket) with CloudFront distributions for edge caching and HTTPS termination
  • DNS: Route53 hosted zone managing both apex and subdomain routing, with ACM certificate validation CNAMEs
  • Payment processing: Stripe Checkout integration with webhook validation and secure key management via Vercel environment variables

Printful API Integration and Variant Population

The Printful integration required fetching product variant IDs to populate the Next.js form. The project stores credentials in .env.local (development) and Vercel's environment variable dashboard (production).

The variant fetching script scripts/get-printful-variants.js makes authenticated requests to the Printful API:

GET https://api.printful.com/store/{store_id}/products
Authorization: Bearer {PRINTFUL_API_KEY}

The script parses the response and extracts variant IDs for the Bella+Canvas 3001 blank in Black, storing them in the site's configuration. This approach avoids hardcoding variant IDs and makes the site portable across Printful stores.

Environment Configuration and Secrets Management

The .env.local file structure is:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=...
GOOGLE_APPS_SCRIPT_URL=https://script.google.com/macros/d/{DEPLOYMENT_ID}/userweb

Development uses a local .env.local file. For production, each variable was registered in Vercel's dashboard via:

vercel env add NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
vercel env add STRIPE_SECRET_KEY
vercel env add PRINTFUL_API_KEY

The NEXT_PUBLIC_ prefix exposes the Stripe publishable key to client-side code (safe—it's meant to be public). The secret key remains server-side only and is used in API routes for Stripe webhook verification and Printful requests.

Vercel Deployment Pipeline

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

npx vercel@latest --prod

This command:

  • Triggers a production build, running TypeScript compilation and Next.js optimization
  • Deploys all five API routes: /api/checkout, /api/webhook, /api/orders, /api/products, and a health check
  • Registers environment variables in the Vercel dashboard so they're injected at build and runtime
  • Assigns a Vercel URL (*.vercel.app) and waits for DNS propagation

After production deployment, DNS records for 86dfrom.com were configured to point to Vercel's nameservers, enabling the custom domain.

AWS Infrastructure: S3, CloudFront, and Route53

In parallel with the Vercel deployment, AWS resources were provisioned for the static CDN and domain management:

S3 Buckets

Two S3 buckets were created:

  • 86dfrom.com — Primary bucket for static site assets (HTML, CSS, JS images). Bucket policy restricts access to CloudFront only via Origin Access Identity (OAI), preventing direct HTTP access.
  • 86from.com — Redirect bucket configured to redirect all requests to 86dfrom.com via HTTP 301 permanent redirects.

The S3 bucket policy for the primary bucket uses a deny-all default with an allow statement for the CloudFront OAI principal:

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {OAI_ID}"
  },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::86dfrom.com/*"
}

CloudFront Distributions

Two CloudFront distributions were created:

  • Primary distribution (86dfrom.com)
    • Origin: 86dfrom.com.s3.amazonaws.com
    • Default root object: index.html
    • Error responses: 404 → index.html (allows SPA-style routing)
    • Viewer protocol policy: Redirect HTTP to HTTPS
    • Certificate: AWS Certificate Manager (ACM) cert for 86dfrom.com
  • Redirect distribution (86from.com)
    • Custom Lambda@Edge function deployed to viewer request stage
    • Function returns HTTP 301 redirects to https://86dfrom.com/path
    • Certificate: ACM cert for 86from.com

ACM Certificates and DNS Validation

Two ACM certificates were requested:

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

ACM returns DNS validation records (CNAME pairs) that must be added to the domain's hosted zone. These were added to Route53:

aws route53 change-resource-record-sets \
  --hosted-zone-id {ZONE_ID} \
  --change-batch file://validation-records.json

ACM validates ownership by checking for these CNAMEs, then marks the certificate status as "Issued." This typically completes within minutes.

Route53 Hosted Zone and DNS Records

A Route53 hosted zone was created for 86dfrom.com with the