Building a Print-on-Demand T-Shirt Site with Next.js 14, Google Apps Script, and AWS CDN Infrastructure
We recently built 86dfrom.com, a serverless print-on-demand t-shirt storefront that integrates Printful's API with a Next.js 14 frontend and Google Apps Script backend. This post walks through the architecture decisions, infrastructure setup, and deployment pipeline that powers the site.
Project Architecture Overview
The site is structured as a three-tier system:
- Frontend: Next.js 14 static export deployed to S3 + CloudFront
- Backend (API): Vercel serverless functions handling Stripe payments and order webhooks
- Fulfillment Automation: Google Apps Script triggered by Stripe webhook events to sync orders to Printful
This hybrid approach lets us leverage managed services (Vercel for stateless API logic, Google Apps Script for long-running fulfillment tasks) while keeping static assets on a CDN for fast global delivery.
Directory Structure and File Organization
The project lives at /Users/cb/Documents/repos/sites/86dfrom.com with this layout:
86dfrom.com/
├── site/
│ ├── index.html # Main storefront (static export)
│ └── success.html # Post-purchase confirmation page
├── gas/
│ ├── Code.gs # Google Apps Script fulfillment logic
│ ├── appsscript.json # GAS project manifest
│ └── .clasp.json # Clasp CLI config for deployment
├── scripts/
│ └── deploy.sh # Bash script for S3 + CloudFront updates
├── .env.local # Environment variables (git-ignored)
└── next/ # (Next.js app in separate directory)
├── app/
│ ├── api/
│ │ ├── variants/route.js # Fetch Printful variant IDs
│ │ ├── checkout/route.js # Stripe Checkout session
│ │ └── webhook/route.js # Stripe webhook handler
│ └── page.js
├── package.json
└── .env.local
The site/ directory contains static HTML that gets deployed to S3, while next/ holds the Vercel app. The separation lets us cache-bust static assets independently of API changes.
Printful Integration: Fetching Variant IDs
The Printful API returns product variants by SKU. For the Bella+Canvas 3001 Black shirt, Printful's catalog includes variants for different sizes (XS–3XL). Rather than hardcode IDs, we created a script to fetch them dynamically.
The api/variants/route.js endpoint makes authenticated requests to Printful:
// Pseudo-code structure
const response = await fetch('https://api.printful.com/products', {
headers: {
'Authorization': `Bearer ${process.env.PRINTFUL_API_KEY}`
}
});
const variants = response.json();
// Filter for Bella+Canvas 3001 Black (SKU matching)
const blackVariants = variants.filter(v => v.sku.includes('3001-BLACK'));
The five variants for sizes XS, S, M, L, XL map to Printful IDs 4016–4020. These IDs are baked into the checkout flow so that when a customer selects a size, we know which Printful variant to order.
AWS Infrastructure: S3 + CloudFront + Route53
Static assets are hosted on AWS to achieve sub-100ms global latency and eliminate Vercel's bandwidth costs for static files.
S3 Bucket Setup
Created bucket 86dfrom-com-site (region: us-east-1) with:
- Bucket policy: Allows CloudFront origin access identity (OAI) to read objects
- No public ACL: All access goes through CloudFront; direct S3 URLs are blocked
- Versioning disabled: Not needed for cache-busted static files
CloudFront Distribution
Distribution ID: E2P8XYZABC123 (example; actual ID varies)
- Origin:
86dfrom-com-site.s3.us-east-1.amazonaws.com - Origin Access Identity: Restricts S3 access to CloudFront only
- Default TTL: 3600 seconds (1 hour) for HTML; 31536000 (1 year) for versioned JS/CSS
- Cache behaviors:
/success.htmlalways revalidates (no cache);*.htmlchecks origin every hour - Compression: Enabled for gzip + brotli
- HTTP/2: Enabled for multiplexing
Route53 DNS
Hosted zone 86dfrom.com with these records:
A (alias):86dfrom.com→ CloudFront distributionA (alias):www.86dfrom.com→ CloudFront distributionMX: Email routing (if transactional emails are sent)
ACM certificate (*.86dfrom.com) was validated via DNS CNAME challenges added to Route53. CloudFront uses this cert for HTTPS termination.
Google Apps Script for Fulfillment
The gas/Code.gs file contains the fulfillment engine that runs when Stripe sends a webhook event. This approach decouples payment processing (Vercel) from order fulfillment (GAS), so if Printful's API is slow, it doesn't block the checkout confirmation.
// Simplified GAS handler
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
if (payload.type === 'charge.succeeded') {
const orderId = payload.data.metadata.order_id;
const items = payload.data.metadata.items; // JSON array
// Call Printful API to create order
const printfulOrder = createPrintfulOrder(orderId, items);
// Log to Sheets for manual review
logOrderToSheets(orderId, printfulOrder);
return ContentService.createTextOutput(JSON.stringify({status: 'ok'}))
.setMimeType(ContentService.MimeType.JSON);
}
}
This runs on Google's infrastructure (no server to manage) and has built-in scaling. The appsscript.json manifest declares the API scopes needed (Sheets, URL Fetch).
Deployment Pipeline
Static Site Deploy (scripts/deploy.sh)