```html

Building a Printful + Stripe T-Shirt Commerce Site: Next.js 14 to AWS CloudFront Infrastructure

This post documents the complete build and deployment of 86dfrom.com, a print-on-demand t-shirt storefront integrating Printful's product catalog with Stripe payment processing. The project demonstrates a modern full-stack architecture spanning Next.js 14 API routes, Google Apps Script webhooks, and AWS infrastructure (S3 + CloudFront + Route53).

What Was Built

86dfrom.com is a single-product t-shirt e-commerce site with the following architecture:

  • Frontend: Next.js 14 with server-side rendered product pages and variant selection
  • Backend: Next.js API routes handling Printful variant fetching and Stripe payment intent creation
  • Fulfillment: Printful integration via REST API for real-time variant data and order fulfillment
  • Payments: Stripe payment processing with webhook validation and order confirmation
  • Infrastructure: AWS S3 static hosting with CloudFront CDN, Route53 DNS, and ACM SSL certificates
  • CRM: Google Apps Script backend for order data capture and email notifications

Project Structure

The repository is organized as follows:

86dfrom.com/
├── site/                    # Static HTML for S3 hosting
│   ├── index.html          # Product page
│   └── success.html        # Order confirmation
├── gas/                    # Google Apps Script
│   ├── Code.gs             # Webhook handler and Sheets integration
│   ├── appsscript.json     # GAS manifest
│   └── .clasp.json         # Clasp project config
├── scripts/
│   ├── deploy.sh           # S3 + CloudFront deployment script
│   └── get-printful-variants.js  # API variant fetcher
├── .env.local              # Environment secrets (not in repo)
└── README.md

The initial development was bootstrapped in /Users/cb/Desktop/86dfrom, then migrated to the canonical repo at ~/Documents/repos/sites/86dfrom.com for long-term maintenance.

Printful Integration: Variant Data Pipeline

The core commerce logic centers on fetching product variants from Printful's API. The Bella+Canvas 3001 Black t-shirt is configured with five size variants (XS–2XL), each with a unique Printful product ID.

Rather than hardcoding variant IDs, we built scripts/get-printful-variants.js to dynamically query the Printful API:

node scripts/get-printful-variants.js

This script authenticates via the Printful API key (stored in .env.local) and outputs variant IDs in the format:

XS: 4016
S: 4017
M: 4018
L: 4019
2XL: 4020

These IDs are then embedded in the Next.js API route /api/variants, which serves them to the frontend on page load. This decouples variant management from code changes—if Printful changes product structure, only the script needs re-running.

API Routes and Stripe Payment Flow

Two critical API routes power the checkout experience:

  • /api/variants — Returns available sizes, prices, and Printful product IDs. Called on page load to populate the size selector.
  • /api/create-payment-intent — Accepts a POST with variantId and quantity, creates a Stripe PaymentIntent, and returns the client secret. The frontend then uses Stripe.js to display the payment form.
  • /api/webhook — Receives payment_intent.succeeded events from Stripe, validates the signature, and forwards order data to Google Apps Script for fulfillment and CRM logging.

The payment flow is intentionally simple: no shopping cart, no inventory. One variant, one quantity selection, one payment. This minimizes complexity and Stripe fee structure.

Google Apps Script Webhook Handler

Rather than stand up a separate Node backend for order processing, we leverage Google Apps Script (GAS) as a lightweight CRM. The file gas/Code.gs contains two functions:

function doPost(e) {
  // Validates webhook signature from /api/webhook
  // Logs order data to a Google Sheet
  // Sends confirmation email to customer
}

function sendEmailConfirmation(email, orderId, variant, quantity) {
  // Composes and sends email via Gmail
}

The appsscript.json manifest declares the script as a web app (executeAs: "ME"), allowing it to receive POST requests at a public URL. The webhook URL is set in the Stripe dashboard under Developers → Webhooks.

Deployment of GAS is handled by clasp (command-line apps script), configured in .clasp.json. This keeps the Google Sheets integration and email logic separate from the main Next.js codebase—a clean separation of concerns.

AWS Infrastructure: S3, CloudFront, and Route53

The static site assets (HTML, CSS, JS) are deployed to an S3 bucket named 86dfrom.com-static. This bucket is configured for web hosting with a bucket policy allowing public reads on all objects:

{
  "Effect": "Allow",
  "Principal": "*",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::86dfrom.com-static/*"
}

Why not use S3 static website hosting directly? Two reasons:

  1. CloudFront caching and edge locations: CloudFront caches objects globally, reducing latency for end users. S3 static websites have no built-in CDN.
  2. SSL/TLS termination at edge: CloudFront distributions can use ACM certificates to serve HTTPS with custom domains. S3 static websites require routing through CloudFront anyway if you want a custom domain and HTTPS.

The CloudFront distribution for 86dfrom.com is configured as follows:

  • Origin: 86dfrom.com-static.s3.us-west-2.amazonaws.com
  • Domain name: d[random-string].cloudfront.net (auto-generated)
  • ACM certificate: Covers both 86dfrom.com and *.86dfrom.com, validated via Route53 DNS CNAME records
  • Cache behaviors: Default TTL 86400 seconds (1 day), max TTL 31536000 (1 year)
  • Viewer protocol policy: Redirect HTTP to HTTPS

A second CloudFront distribution was created for the subdomain 86from.com (typo handling). This distribution uses a CloudFront Function (inline JavaScript) to permanently redirect all