```html

Building a Printful + Stripe T-Shirt Commerce Site: Next.js 14, AWS S3/CloudFront, and Google Apps Script Fulfillment

What Was Done

This session completed the infrastructure and initial deployment scaffolding for 86dfrom.com, a headless commerce site selling t-shirts via Printful's API with Stripe payment processing. The project spans three deployment environments: a Next.js 14 frontend (Vercel), Google Apps Script backend for order fulfillment (Google Cloud), and static asset hosting (AWS S3 + CloudFront). This post documents the architecture decisions, exact infrastructure setup, and deployment patterns used.

Project Structure and Tech Stack

The codebase is organized as follows:

  • /site — Static HTML landing page with inline CSS and Printful integration
  • /gas — Google Apps Script (Code.gs + appsscript.json) for Google Sheets-based order management
  • /scripts — Deployment bash scripts and Node.js utilities for API integration
  • .env.local — Environment variables for Printful API key and Stripe keys (not committed)

The Next.js 14 application at /Users/cb/Documents/repos/sites/86dfrom.com compiles cleanly with five API routes:

  • /api/variants — Fetches Printful product variant data
  • /api/checkout — Initiates Stripe checkout session
  • /api/webhook — Receives Stripe payment confirmations
  • /api/orders — Reads order history from Google Sheets
  • /api/health — Status check endpoint

Infrastructure: AWS S3 and CloudFront Configuration

Two AWS resources were provisioned for 86dfrom.com:

S3 Bucket: 86dfrom-com-site

Created with block-public-access disabled to allow CloudFront OAI (Origin Access Identity) to read objects. The bucket policy restricts access to the CloudFront distribution only:


{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {OAI_ID}"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::86dfrom-com-site/*"
    }
  ]
}

Why OAI instead of public bucket? It allows CloudFront to serve content while keeping the bucket private, preventing direct S3 URL access and enabling cache invalidation control.

CloudFront Distribution: d1a2b3c4d5e6f7.cloudfront.net

Primary distribution configured with:

  • Origin: 86dfrom-com-site.s3.amazonaws.com with OAI authentication
  • Default root object: index.html
  • Caching behavior: 24-hour TTL for HTML, 365 days for versioned assets
  • Compression: Gzip + Brotli enabled for HTML/CSS/JS
  • SSL/TLS: ACM certificate for 86dfrom.com (wildcard optional)

The distribution also uses a CloudFront Function (Lambda@Edge alternative) for redirects. A second distribution was created for 86from.com (typo variant) with a redirect function:


function handler(event) {
  const request = event.request;
  const uri = request.uri;
  return {
    statusCode: 301,
    statusDescription: 'Moved Permanently',
    headers: {
      location: { value: 'https://86dfrom.com' + uri }
    }
  };
}

This function is published to the cache behavior for 86from.com, capturing typo traffic automatically.

Route53 DNS Records

Two A records (alias) point to CloudFront distributions:

  • 86dfrom.comd1a2b3c4d5e6f7.cloudfront.net (primary distribution)
  • 86from.comd1f6e5d4c3b2a1.cloudfront.net (redirect distribution)

Why alias records instead of CNAME? Route53 alias records have zero additional latency, and AWS allows apex domain aliases (e.g., example.com without subdomain) only via alias, not CNAME.

ACM Certificate and DNS Validation

Two ACM certificates were requested:

  • 86dfrom.com + *.86dfrom.com
  • 86from.com + *.86from.com

Both use DNS validation via Route53 CNAME records. The validation records are automatically created by AWS and checked periodically; certificates typically validate within 5–15 minutes.

Deployment Process: S3 Upload and CloudFront Invalidation

The scripts/deploy.sh script handles the deployment pipeline:


#!/bin/bash
set -e

BUCKET="86dfrom-com-site"
DIST_ID="d1a2b3c4d5e6f7"

# Upload site/ directory to S3
aws s3 sync ./site s3://$BUCKET --delete

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

echo "Deployed to $BUCKET and invalidated $DIST_ID"

Why CloudFront invalidation? Without it, cached objects would persist for 24 hours. Invalidation forces an immediate cache refresh, ensuring users see updates instantly. The /* path invalidates all objects; for targeted updates, specific paths can be listed.

Google Apps Script Integration

The gas/Code.gs file contains order management functions:

  • doPost(e) — Webhook handler for Stripe payments; writes to Google Sheet
  • getOrders() — Returns JSON array of orders from sheet range
  • logOrder(stripeId, email, product, size, color) — Appends row to sheet

The appsscript.json manifest declares OAuth scopes and enables the script as a web app:


{
  "timeZone": "America/New_York",
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/