Building a Printful-Integrated T-Shirt Store: Next.js 14, Google Apps Script, and AWS CloudFront Infrastructure

What Was Done

We built an end-to-end t-shirt e-commerce site for 86dfrom.com integrating Printful's on-demand fulfillment API, Stripe payment processing, and Google Apps Script for order management. The infrastructure spans a Next.js 14 application deployed to Vercel, static assets cached through AWS CloudFront, and a serverless Apps Script backend for Sheets integration.

The project required standing up multiple AWS resources (S3, CloudFront, Route53, ACM certificates), configuring DNS for two domains (86dfrom.com and 86from.com as a redirect), and structuring a monorepo that cleanly separates the web application, deployment scripts, and Apps Script code.

Technical Architecture

Repository Structure

The project lives at ~/Documents/repos/sites/86dfrom.com/ with this structure:

86dfrom.com/
├── site/                    # Static HTML entry point
│   ├── index.html          # Landing page with Stripe form
│   └── success.html        # Post-purchase confirmation
├── gas/                     # Google Apps Script backend
│   ├── Code.gs             # Main Apps Script logic
│   ├── appsscript.json     # GAS manifest
│   └── .clasp.json         # Clasp deployment config
├── scripts/
│   ├── deploy.sh           # S3 + CloudFront invalidation
│   └── get-printful-variants.js  # Fetch variant IDs from Printful API
└── .env.local              # Runtime secrets (Printful key, Stripe keys)

Technology Stack

  • Web Hosting: Vercel (Next.js 14 app with API routes)
  • Static Assets: AWS S3 (86dfrom.com-static bucket) + CloudFront distribution
  • DNS: Route53 (hosted zone for 86dfrom.com)
  • SSL/TLS: AWS Certificate Manager (ACM) with DNS validation
  • Payment Processing: Stripe (test or live keys)
  • Fulfillment: Printful API with OAuth token authentication
  • Order Management: Google Apps Script + Sheets integration
  • Domain Redirect: Secondary CloudFront distribution for 86from.com (typo variant)

Infrastructure Setup Details

AWS Resources Provisioned

S3 Bucket: Created 86dfrom.com-static with public read access policy. This bucket serves as the origin for CloudFront and holds the static site/index.html and site/success.html.

ACM Certificates: Requested two certificates via aws acm request-certificate:

  • Primary: 86dfrom.com (DNS validation via Route53 CNAME records)
  • Secondary: 86from.com (typo redirect domain)

DNS validation records were created as Route53 CNAME records in the hosted zone, with validation completing within minutes. This automated DNS-based approach (vs. email) ensures we can programmatically verify domain ownership without manual intervention.

CloudFront Distributions:

  • Primary (86dfrom.com): Origin points to S3 bucket. Cache TTL set to 3600 seconds for HTML (allowing rapid iteration during development). Distribution ID used for invalidation after each deploy.
  • Secondary (86from.com): Uses CloudFront Functions to redirect all traffic to https://86dfrom.com with a 301 permanent redirect. This captures typo traffic without a separate backend.

Both distributions enforce HTTPS-only connections and use the ACM certificates provisioned above.

Route53 Hosted Zone: Created for 86dfrom.com with A-records pointing to both CloudFront distributions. The primary domain's A-record uses CloudFront's alias target; the typo domain uses the redirect distribution's alias.

Deployment Pipeline

The scripts/deploy.sh script automates static asset updates:

#!/bin/bash
# Copy site/ files to S3
aws s3 sync ./site s3://86dfrom.com-static --delete

# Invalidate CloudFront cache
aws cloudfront create-invalidation \
  --distribution-id [DIST_ID] \
  --paths "/*"

This two-step process ensures updates are live within seconds (S3 sync) and the CDN cache is purged immediately (CloudFront invalidation). The --delete flag removes any orphaned files from S3.

Application Layer: Next.js + Printful + Stripe

API Route Structure

The Next.js app at the Vercel-hosted domain (separate from the static CloudFront domain) exposes these endpoints:

  • /api/variants – Returns the 5 variant IDs (Black Bella+Canvas 3001, sizes XS–2XL) fetched from Printful
  • /api/checkout – Stripe Checkout session creation; calls Printful API to stage order
  • /api/webhook – Stripe webhook receiver for payment_intent.succeeded events; triggers Apps Script webhook to log order

Printful Integration

The script scripts/get-printful-variants.js queries Printful's REST API (https://api.printful.com) to fetch product variants:

curl -H "Authorization: Bearer [PRINTFUL_API_KEY]" \
  https://api.printful.com/store/products

This runs once to populate the .env.local file with variant IDs for each size. The Printful API key (scoped to the 86Store account) is stored as an environment variable and used by /api/checkout to create draft orders with real-time fulfillment costs.

Why this approach? Printful's pricing is dynamic (based on materials, location, volume). Rather than hardcoding prices, the checkout endpoint fetches live costs from Printful, ensuring accuracy. The variant IDs link each size to Printful's inventory system.

Stripe Payment Flow

The frontend form posts to /api/checkout, which:

  1. Validates the order (size, quantity, email)
  2. Fetches cost from Printful for that variant + quantity
  3. Creates a Stripe Checkout session with that amount
  4. Returns the session URL to redirect the user

After successful payment, Stripe fires a payment_intent.succeeded webhook to /api/webhook, which:

  1. Verifies the webhook signature using the webhook secret (stored in Vercel env vars)
  2. Extracts customer email and order metadata