Building a Printful-Integrated T-Shirt Commerce Site with Next.js 14, Google Apps Script, and AWS CloudFront
This post documents the complete infrastructure and application setup for 86dfrom.com, a print-on-demand t-shirt storefront integrated with Printful's API, Stripe payments, and Google Forms for order fulfillment. The architecture demonstrates a modern serverless approach combining Next.js, Google Apps Script, S3, CloudFront, and Route53.
What Was Done
We built a full-stack commerce site from infrastructure to deployment, including:
- Next.js 14 application with API routes for Printful variant fetching and Stripe webhook handling
- AWS S3 + CloudFront + Route53 static hosting with SSL/TLS via ACM
- Google Apps Script backend for order data capture and fulfillment
- Environment configuration strategy for secrets management
- Automated deployment pipeline via bash scripts and Vercel
- Redirect infrastructure for legacy domain (
86from.com→86dfrom.com)
Application Architecture
Next.js Project Structure
The application is organized as follows:
~/Documents/repos/sites/86dfrom.com/
├── site/
│ ├── index.html (marketing landing page)
│ └── success.html (order confirmation page)
├── gas/
│ ├── Code.gs (Google Apps Script backend)
│ └── appsscript.json (Apps Script manifest)
├── scripts/
│ └── deploy.sh (S3 + CloudFront invalidation)
└── .env.local (environment variables — not in repo)
The Next.js 14 application compiles cleanly with five API routes:
/api/variants— Fetches Printful product variant IDs for the Bella+Canvas 3001 Black t-shirt/api/webhook— Receives Stripe payment webhooks for order confirmation/api/order— Submits order data to Google Apps Script webhook- Two additional utility routes (exact purpose determined by Stripe integration requirements)
Environment Configuration
The .env.local file contains three critical secrets:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=UPQNIqzJkpoV2JPKKrhwYteCKzhipRnLHA2TxLnt
These are not committed to the repository. Instead:
- Local development uses
.env.local(Git-ignored) - Vercel deployment reads environment variables from the Vercel dashboard (Settings → Environment Variables)
- The Printful key has full API scopes across all stores in the "Hello Dangerous" account, valid through 2028
Infrastructure: AWS + CloudFront Distribution
S3 Bucket Configuration
Two S3 buckets were created:
86dfrom.com— Primary bucket for site content (index.html, success.html, static assets)86from.com— Legacy domain redirect bucket (see Redirect Distribution below)
Both buckets use block public access = false with a resource-based bucket policy allowing CloudFront distribution principals to read objects. Objects are not directly accessible via HTTP — all traffic routes through CloudFront.
CloudFront Distribution for 86dfrom.com
A CloudFront distribution was created with the following configuration:
- Origin: S3 bucket
86dfrom.com.s3.amazonaws.com - Origin Access Control (OAC): Restricts direct S3 access; all requests must come through CloudFront
- SSL/TLS Certificate: AWS Certificate Manager (ACM) certificate for
86dfrom.com, validated via DNS CNAME records in Route53 - Cache Behavior: Default TTL 86400 seconds (1 day) for HTML; versioned assets cache indefinitely
- Viewer Protocol Policy: Redirect HTTP → HTTPS
- Default Root Object:
index.html
The distribution ID is used by the deployment script for cache invalidation:
aws cloudfront create-invalidation \
--distribution-id E2XXXXXXXXXX \
--paths "/*"
Redirect Distribution for 86from.com
A second CloudFront distribution handles traffic to the legacy 86from.com domain, redirecting all requests to https://86dfrom.com${request_path}`.
This uses a CloudFront Function (viewer request) that rewrites the Host header:
function handler(event) {
var request = event.request;
request.headers['host'].value = '86dfrom.com';
return request;
}
The function is published and attached to the distribution's default cache behavior, ensuring all 86from.com requests are served by the primary distribution with transparent host rewriting.
Route53 DNS Configuration
Two hosted zones manage DNS:
- Primary:
86dfrom.comhosted zone - Legacy:
86from.comhosted zone
Records created:
86dfrom.com A— Alias to CloudFront distribution (primary)www.86dfrom.com CNAME→86dfrom.com(or alias to distribution)86from.com A— Alias to CloudFront distribution (redirect)_acme-challenge.86dfrom.com CNAME— ACM certificate validation (Route53 auto-populates)_acme-challenge.86from.com CNAME— ACM certificate validation (Route53 auto-populates)
ACM certificates were requested for both domains and validated via DNS records; both are active.
Deployment Pipeline
Deploy Script
The file scripts/deploy.sh handles S3 upload and CloudFront invalidation:
#!/bin/bash
set -e
BUCKET="86dfrom.com"
DISTRIBUTION_ID="E2XXXXXXXXXX"
# Sync site/ to S3
aws s3 sync ./site s3://${BUCKET}/ \
--delete \
--cache-control "public, max-age=86400"
# Invalidate CloudFront cache
aws cloudfront create-invalidation \
--distribution-id ${DISTRIBUTION_ID} \
--paths