Building a Printful-Integrated T-Shirt Commerce Site: Next.js 14, Google Apps Script, and AWS CDN Architecture
This post documents the infrastructure and deployment pipeline built for 86dfrom.com, a print-on-demand t-shirt storefront integrating Printful's API with a Next.js 14 frontend, Google Apps Script backend, and AWS CloudFront distribution.
Project Structure and Technology Stack
The project is organized into three distinct layers:
- Frontend: Next.js 14 with client-side variant selection and checkout flow at
/site - Serverless Backend: Google Apps Script (GAS) for Stripe webhook handling and order processing at
/gas - Infrastructure: AWS S3, CloudFront, Route53, and ACM for static hosting and CDN delivery
The repository structure mirrors this separation:
86dfrom/
├── site/
│ ├── index.html (Main storefront)
│ └── success.html (Post-purchase confirmation)
├── gas/
│ ├── Code.gs (Apps Script webhook handler)
│ └── appsscript.json (GAS manifest)
├── scripts/
│ └── deploy.sh (S3 + CloudFront invalidation)
└── .env.local (API keys and secrets)
Why Printful + Google Apps Script Instead of Traditional Backend?
For a small-footprint commerce site, this hybrid approach offers several advantages:
- Zero server management: Google Apps Script runs on Google's infrastructure with no cold starts for webhooks
- Native Stripe integration: GAS can directly call Stripe APIs and Apps Script receives webhook payloads via POST
- Cost: Printful handles fulfillment; we pay only for Stripe processing and domain hosting
- Simplicity: Static site on S3/CloudFront means CDN caching, low latency, and predictable costs
The alternative—running a Node.js server on Vercel or EC2—adds unnecessary complexity when Printful already provides the product catalog and fulfillment logistics.
Frontend Architecture: Next.js 14 with Variant Selection
The Next.js application exposes five API routes for the storefront:
/api/variants— Returns hardcoded Printful variant IDs for the Bella+Canvas 3001 Black unisex tee (variants 4016–4020 for XS–2XL)/api/checkout— Accepts customer data and creates a Stripe checkout session/api/webhook— Receives Stripe webhook events (payment_intent.succeeded, charge.refunded)/api/order— Retrieves order status from Google Apps Script cache/api/health— Simple status check for monitoring
The frontend hydrates the DOM with variant selection dropdowns, validates user input, and redirects to checkout.stripe.com after initiating a session. On successful payment, Stripe redirects to /success.html, which polls /api/order to display tracking information once the order is dispatched by Printful.
Why static HTML files instead of SSR? The variant data is fetched client-side via JavaScript, eliminating the need for server-side rendering. This keeps the site cacheable and allows CloudFront to serve the HTML with a long TTL (3600 seconds).
Infrastructure: S3, CloudFront, and Route53 Setup
The site is deployed to AWS using the following pattern:
S3 Bucket Configuration
Bucket name: 86dfrom.com (in us-east-1 for CloudFront origin)
The bucket is configured as a static website origin (not for public read access). A bucket policy restricts access to the CloudFront distribution using an Origin Access Identity (OAI):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity "
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::86dfrom.com/*"
}
]
}
This ensures only CloudFront can read objects, preventing direct S3 URL access and enforcing CDN caching policies.
CloudFront Distribution for 86dfrom.com
Distribution ID: Assigned after creation (used for cache invalidation)
Configuration details:
- Origin:
86dfrom.com.s3.us-east-1.amazonaws.com(S3 origin with OAI) - Default root object:
index.html - Viewer protocol policy: Redirect HTTP to HTTPS
- Cache behavior: 3600-second TTL for HTML; 86400 for assets (CSS, JS)
- SSL certificate: ACM certificate for
86dfrom.com(validated via DNS CNAME in Route53) - Custom headers: Security headers (Content-Security-Policy, X-Frame-Options, Strict-Transport-Security)
CloudFront functions are deployed to handle request rewrites (e.g., trailing slashes, index.html routing) and HTTP-to-HTTPS redirects without touching Lambda@Edge (simpler to maintain, lower latency).
Route53 and DNS Configuration
Two Route53 records point to CloudFront:
86dfrom.com— A record aliased to the primary CloudFront distributionwww.86dfrom.com— CNAME to the same CloudFront distribution
ACM certificate validation required adding CNAME records to Route53 (AWS handles this automatically if you manage DNS through Route53). Once validated, the certificate is attached to the CloudFront distribution, enabling HTTPS for all visitors.
Why not use Vercel for this? While Vercel is excellent for dynamic Next.js apps, this site has minimal server-side logic. S3 + CloudFront is more cost-effective for static content and offers tighter control over caching headers and security policies. Additionally, the team already manages AWS infrastructure (as evidenced by existing CloudFront distributions for other properties), so keeping 86dfrom on the same platform reduces operational overhead.
Deployment Pipeline
The deployment script (scripts/deploy.sh) handles two responsibilities:
- Sync the
site/directory to S3, excluding.env.local - Invalidate the CloudFront distribution cache to force a refresh
#!/bin/bash
set -e
AWS_REGION="us-east-1"
S3_BUCKET="86dfrom.com"
CF_DISTRIBUTION_ID="