```html

Building a Printful-Integrated T-Shirt E-Commerce Site with Next.js 14, Google Apps Script, and AWS CloudFront

This post documents the full-stack development and deployment of 86dfrom.com, a print-on-demand t-shirt storefront powered by Printful's API, Stripe payments, and a serverless backend. We'll cover the architecture, deployment pipeline, and why certain technical decisions were made.

Project Overview

The goal was to build a lightweight, fast e-commerce site that:

  • Displays Bella+Canvas 3001 black t-shirts with dynamic variant selection
  • Integrates with Printful's REST API to fetch real-time product variants
  • Processes payments via Stripe with webhook validation
  • Serves both a public storefront and a success confirmation page
  • Maintains a Google Apps Script backend for order fulfillment logging

Tech Stack

  • Frontend: Next.js 14 (App Router), deployed on Vercel
  • Backend APIs: Next.js API routes (/app/api/)
  • Order Fulfillment: Google Apps Script (GAS) with appsscript.json` manifest
  • Payment Processing: Stripe (REST API + webhooks)
  • Product Data: Printful API (variant IDs, pricing)
  • DNS & CDN: Route53 + CloudFront
  • Static Assets: S3 (future static site fallback)

Directory Structure


/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│   ├── index.html          (legacy static fallback)
│   └── success.html        (order confirmation page)
├── gas/
│   ├── Code.gs             (Google Apps Script functions)
│   ├── appsscript.json     (GAS manifest with Sheets bindings)
│   └── .clasp.json         (clasp CLI config for GAS deployment)
├── scripts/
│   └── deploy.sh           (S3 + CloudFront invalidation script)
├── .env.local              (runtime secrets: Stripe, Printful, GAS webhook)
└── [Next.js app files]

Core Architecture Decisions

Why Printful API Over Manual Inventory?

Printful's REST API provides real-time variant availability and pricing without maintaining our own database. We fetch variant IDs (e.g., 4016 for XS Black, 4017 for S Black) once at build time, then store them in .env.local. This avoids:

  • Database latency for simple product data
  • Manual SKU management across multiple print providers
  • Inventory sync complexity

A simple scripts/get-printful-variants.js` fetches variant metadata from Printful's API and prints them to stdout, which we then hardcode into environment variables.

Why Google Apps Script for Order Logging?

Instead of a traditional database, we use Google Apps Script bound to a Google Sheet. When a Stripe webhook fires at /api/webhook, the Next.js backend invokes the GAS webhook URL (stored in .env.local as GOOGLE_APPS_SCRIPT_WEBHOOK_URL) with order details. GAS appends a row to the sheet, creating an audit trail without server infrastructure.

Why this pattern?

  • No database to provision or monitor
  • Familiar spreadsheet interface for non-technical team members
  • Built-in email notifications via Google Sheets
  • Version history and access controls are free

Why Stripe Webhooks Over Polling?

Rather than polling Stripe's API for payment status, we register a webhook endpoint at /api/webhook in the Stripe dashboard. Stripe sends a POST request with event data (e.g., payment_intent.succeeded) as soon as the payment completes. This:

  • Reduces API calls and latency
  • Guarantees delivery (with exponential backoff retries)
  • Allows synchronous order logging before the customer sees a success page

Infrastructure & Deployment

DNS & SSL

The domain 86dfrom.com is managed in AWS Route53. We requested an ACM certificate for 86dfrom.com with DNS validation, which required adding a CNAME record in Route53 pointing to AWS's validation endpoint. Once validated, the certificate is used by CloudFront.

Similarly, 86from.com (no 'd') was set up as a redirect distribution: a CloudFront function at the viewer-request stage intercepts traffic and redirects to 86dfrom.com.

CloudFront Distribution

The main CloudFront distribution for 86dfrom.com`:

  • Origin: Vercel deployment (CNAME: 86dfrom-com.vercel.app or direct Vercel domain)
  • SSL Certificate: ACM certificate for 86dfrom.com`
  • Cache Policy: Managed-CachingOptimized for static assets (JS, CSS, images)
  • Origin Request Policy: AllViewerExceptCloudFrontHeaders (forwards cookies and authorization headers to Vercel)
  • Viewer Protocol Policy: Redirect HTTP to HTTPS

Why CloudFront in front of Vercel?

  • Edge caching of static assets (reduced origin requests)
  • Custom domain with our own SSL certificate
  • CloudFront functions for lightweight request transformation (like the 86from.com redirect)
  • DDoS protection via AWS Shield Standard

S3 Bucket (Fallback)

A static S3 bucket was provisioned (86dfrom-com-s3) as a potential fallback and for hosting the legacy HTML files. Its bucket policy restricts access to CloudFront only, preventing direct public access. The scripts/deploy.sh script copies site/index.html and site/success.html to S3 and invalidates the CloudFront cache:


#!/bin/bash
aws s3 cp site/ s3://86dfrom-com-s3/ --recursive
aws cloudfront create-invalidation \
  --distribution-id E2ABC123EXAMPLE \
  --paths "/*"

Route53 Records

Two A records were created in Route53 for 86dfrom.com`:

  • Alias to CloudFront Distribution: Points 86dfrom.com` to the main distribution (no origin IP needed; Route53 automatically resolves the distribution's domain)