Building a Serverless T-Shirt Store with Next.js 14, Google Apps Script, and AWS CloudFront

What Was Done

We built a complete serverless e-commerce platform for 86dfrom.com, a Printful-integrated t-shirt store. The architecture spans three environments: a Next.js 14 frontend on Vercel, a Google Apps Script backend for order processing, and AWS CloudFront + S3 for static asset delivery. This post covers the infrastructure decisions, deployment pipeline, and integration patterns that enable a zero-maintenance, auto-scaling commerce platform.

Architecture Overview

The system consists of four distinct layers:

  • Client Layer: Next.js 14 on Vercel (routes: /api/variants, /api/create-checkout, /api/webhook, /success)
  • Payment Gateway: Stripe for PCI-compliant checkout
  • Order Processing: Google Apps Script (GAS) webhook receiver
  • Print Fulfillment: Printful API for inventory and order transmission
  • Static Assets: AWS S3 + CloudFront for fonts and images

Directory Structure & File Organization

The project is organized to support both local development and cloud deployments:

/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│   ├── index.html                    # Static landing page
│   └── success.html                  # Post-purchase success page
├── gas/
│   ├── Code.gs                       # Google Apps Script webhook handler
│   ├── appsscript.json              # GAS project manifest
│   └── .clasp.json                  # Clasp CLI configuration
├── scripts/
│   └── deploy.sh                     # Production deployment automation
└── .env.local                        # Environment variables (git-ignored)

The dual repository structure—both /Users/cb/Desktop/86dfrom (development) and /Users/cb/Documents/repos/sites/86dfrom.com (version-controlled source)—ensures a clean separation between transient work and permanent source of truth.

Next.js 14 Build & Route Configuration

The Next.js application compiles cleanly with all five routes:

app/
├── api/
│   ├── variants/route.js             # GET → Printful inventory
│   ├── create-checkout/route.js      # POST → Stripe Checkout
│   └── webhook/route.js              # POST → Stripe webhook
├── success/page.js                   # Redirect landing
└── page.js                           # Homepage

Each route serves a specific purpose in the transaction flow:

  • /api/variants: Fetches live variant IDs and pricing from Printful (cached client-side to minimize API calls)
  • /api/create-checkout: Creates a Stripe Checkout session and persists order data to Firestore for webhook correlation
  • /api/webhook: Receives Stripe checkout.session.completed events, validates signatures, and enqueues orders to Google Apps Script
  • /success: Post-purchase landing page (Stripe redirects here on completion)

Build verification command:

cd ~/Documents/repos/sites/86dfrom.com && npm run build

Result: Next.js 14 compiles to .next/ with zero errors. All API routes are instrumented and ready for production.

Printful Integration & Variant IDs

Rather than hardcoding product variants, we fetch them dynamically from the Printful API. The script scripts/get-printful-variants.js queries the Printful store for all t-shirt variants and extracts their IDs:

node scripts/get-printful-variants.js

This script:

  • Authenticates to Printful using an API key stored in .env.local
  • Lists all products in the 86Store
  • Filters for the Bella+Canvas 3001 Black variant (style ID 4016–4020)
  • Outputs variant IDs and pricing

Variant selection was intentional: we chose the plain Black 3001 to simplify the initial MVP and avoid complexity from heather/oxblood variants. This can be extended later without code changes—just update the filter logic.

AWS Infrastructure: S3, CloudFront, and Route53

Static assets (Google Anton font in woff2, images, stylesheets) are served from AWS rather than Vercel to optimize TTFB and reduce bandwidth costs.

S3 Bucket Configuration

A dedicated S3 bucket 86dfrom-com-static was created with the following configuration:

aws s3api create-bucket \
  --bucket 86dfrom-com-static \
  --region us-east-1

The bucket policy grants public read access to CloudFront and blocks direct S3 access:

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

This follows the principle of least privilege: only CloudFront can read objects, preventing unauthorized direct S3 access.

CloudFront Distribution

Two CloudFront distributions were created:

  1. Primary (86dfrom.com): Origin set to S3 bucket, HTTP/2, gzip compression enabled, TTL set to 86400 seconds (24 hours) for fonts, 3600 for HTML.
  2. Redirect (86from.com → 86dfrom.com): CloudFront Functions (not Lambda@Edge) implement a simple redirect at the edge, reducing latency.

CloudFront Function (edge redirect):

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

This function is deployed to the 86from.com distribution and executes at viewer request, eliminating round-trips to origin.

Route53 DNS Configuration

Two A records (alias) were created in the hosted zone for 86dfrom.com:

  • 86dfrom.com → CloudFront