Building a Printful-Integrated T-Shirt Commerce Site: Next.js 14, Google Apps Script, and AWS CDN Architecture

This post covers the infrastructure and development patterns used to build 86dfrom.com, a print-on-demand t-shirt storefront integrated with Printful's API, Stripe payments, and a Google Sheets backend via Google Apps Script. The architecture spans multiple platforms—Next.js 14 on Vercel, serverless functions for webhook handling, and static asset delivery via CloudFront.

Project Structure and Technology Stack

The project is organized into three distinct deployment units:

  • Next.js 14 application (/site root) — handles product pages, checkout flow, and Stripe integration
  • Google Apps Script (/gas) — serverless backend that syncs orders to Google Sheets and manages inventory
  • Deployment automation (/scripts) — bash scripts for reproducible infrastructure-as-code deployments

All source files are version-controlled at ~/Documents/repos/sites/86dfrom.com/, with environment configuration managed through .env.local for local development and Vercel's environment variable UI for production.

Integrating Printful's Product Variant System

The Printful API requires specific variant IDs for each product size/color combination. Rather than hardcoding these, we created a discovery script (scripts/get-printful-variants.js) that queries Printful's REST API and extracts variant metadata for a given product.

The script targets Bella+Canvas 3001 Black t-shirts and fetches all available sizes. Variant IDs (e.g., 4016–4020 for S through 2XL) are persisted to .env.local so route handlers can reference them when:

  • Calculating shipping quotes via /api/shipping
  • Creating draft orders via /api/create-order
  • Validating inventory in real-time

This approach decouples product configuration from code, allowing rapid iterations without redeployment.

Payment Processing and Webhook Architecture

Stripe is configured as the payment processor. The integration consists of:

  • Client-side: Stripe Elements embedded in site/index.html for PCI-compliant card capture
  • API route /api/checkout: Accepts tokenized card data, creates a Stripe PaymentIntent, and returns a client secret for frontend confirmation
  • Webhook route /api/webhook: Listens for payment_intent.succeeded events, validates signatures, and triggers downstream order creation

The webhook secret is configured via Vercel environment variables and never exposed to the client. Webhook validation uses Stripe's SDK to prevent replay attacks and unauthorized requests.

Google Apps Script for Order Persistence

Once a payment succeeds, the backend must log the order and trigger fulfillment. Rather than maintaining a traditional database, we use Google Apps Script (gas/Code.gs) deployed as a web app with appsscript.json configured for:

  • OAuth 2.0 execution under a service account
  • Write access to a shared Google Sheet
  • Time-based triggers for automated syncs

When a payment confirms, the webhook sends order JSON (customer email, shipping address, selected variant ID) to the Apps Script deployment URL. The script:

  • Appends a row to the "Orders" sheet with timestamps
  • Cross-references variant IDs to a "Variants" sheet for SKU lookups
  • Triggers a success email to the customer via site/success.html (rendered server-side or via webhook redirect)

This eliminates the need for a database server while maintaining audit trails in a format accessible to non-technical stakeholders.

Infrastructure: DNS, SSL, and CDN Setup

The 86dfrom.com domain uses AWS infrastructure for static assets and DNS management:

S3 Bucket Configuration

A bucket named 86dfrom-com stores:

  • Minified CSS and JavaScript
  • The success.html thank-you page
  • Product images and brand assets (served via CloudFront)

Bucket policy restricts read access exclusively to the CloudFront distribution's origin access identity (OAI), preventing direct HTTP requests to S3.

ACM Certificate and CloudFront Distribution

Two ACM certificates were provisioned:

  • 86dfrom.com — primary domain, points to CloudFront distribution d[xxxxx].cloudfront.net
  • 86from.com (typo variant) — separate distribution configured with a CloudFront function to redirect https://86from.com/*https://86dfrom.com/* (301 permanent redirect)

DNS validation for both certificates was automated via Route53; CNAMEs were added to the hosted zone and verified within minutes.

Route53 Hosted Zone

The 86dfrom.com hosted zone contains:

  • A record: 86dfrom.com → CloudFront distribution alias
  • CNAME record: www.86dfrom.com → CloudFront distribution (optional; typically aliased to root)
  • A record: 86from.com → separate redirect CloudFront distribution
  • ACM validation CNAMEs (added automatically, can be deleted after cert validation)

Deployment Pipeline

The scripts/deploy.sh script automates the full deployment:

#!/bin/bash
# Example structure (no credentials shown)
export AWS_REGION=us-east-1

# Build static assets
npm run build

# Sync site/ and gas/ directories to S3
aws s3 sync site/ s3://86dfrom-com/ --delete

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

# Deploy Google Apps Script
cd gas && clasp push && cd ..

This pattern ensures:

  • No manual S3 uploads (error-prone, slow)
  • Automatic cache invalidation after each deploy (no stale assets)
  • Both static and serverless components deploy in a single command

Environment Configuration and Secrets Management

Sensitive values (Printful API key, Stripe secret key, webhook secret) are never committed to version control. Instead:

  • .env.local (git-ignored) stores development secrets locally
  • Vercel's environment variable UI stores production secrets, scoped by environment (Preview, Production