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 Stripecheckout.session.completedevents, 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:
- 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.
- 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