Building a Printful-Integrated T-Shirt Commerce Site: Next.js 14, Google Apps Script, and AWS CDN Infrastructure
What Was Done
We deployed a full-stack t-shirt e-commerce site for 86dfrom.com using a modern hybrid architecture: a Next.js 14 frontend with Stripe payment processing, a Google Apps Script backend for order fulfillment, and AWS CloudFront + S3 for static asset delivery. The project integrates with Printful's API for variant management and inventory, uses environment-based configuration for multi-environment deployments, and establishes infrastructure-as-code patterns for future scaling.
Project Structure and Technology Choices
The codebase lives at /Users/cb/Documents/repos/sites/86dfrom.com/ with three core directories:
/site— Static HTML landing pages (index.html, success.html) for the public storefront/gas— Google Apps Script (Code.gs, appsscript.json) handling order webhook processing and fulfillment coordination/scripts— Deployment automation (deploy.sh) and Printful integration tooling
Why this structure? Separation of concerns lets us:
- Deploy the frontend independently via CloudFront invalidation (CDN edge refresh)
- Manage Google Apps Script versioning with clasp (command-line tool), enabling version control of serverless functions
- Maintain shell scripts for repeatable infrastructure operations without manual AWS console clicks
We chose Next.js 14 initially for the Vercel ecosystem integration, but the production deployment actually uses static HTML served from S3 behind CloudFront — a deliberate pivot to eliminate cold starts and reduce compute costs. The Next.js build outputs static pages, which we then push to AWS.
Printful API Integration
The Printful API key is scoped to the 86Store account within the dangerouscentaur.com Printful organization. This allows querying product variants without requiring per-variant hardcoding. The script at /scripts/get-printful-variants.js (referenced in setup but not shown here) makes authenticated requests to https://api.printful.com/store/products/ to fetch variant metadata.
We identified five Bella+Canvas 3001 Black t-shirt variant IDs (4016–4020 in Printful's catalog) as the canonical product line. The variant IDs populate a JSON configuration that the frontend uses to construct size/color selectors and POST requests to the Stripe checkout endpoint.
Why Printful? It abstracts inventory management, print production, and fulfillment. Our Order webhook (explained below) triggers Printful's order creation API, eliminating manual order entry and reducing human error.
Environment Configuration and Secrets Management
The .env.local file (not version-controlled) contains:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY— frontend-safe, sent to browsers for Stripe.js initializationSTRIPE_SECRET_KEY— backend-only, never exposed to clientsPRINTFUL_API_KEY— used by Google Apps Script to create orders in PrintfulDANGEROUSCENTAUR_STRIPE_KEY— legacy reference if multi-tenant support is needed later
Vercel environment variables are set via the web console (or vercel env pull / vercel env add commands) to mirror local .env.local, ensuring production parity without committing secrets to Git.
Google Apps Script for Order Fulfillment
The /gas/Code.gs file contains a webhook receiver function that Stripe calls when a payment succeeds. The flow:
- Stripe sends a
charge.succeededevent to a Google Apps Script web app endpoint - Code.gs parses the event, extracts customer and product data
- It constructs a Printful order object and POSTs to
https://api.printful.com/orders/ - On success, metadata is logged to a Google Sheet for fulfillment tracking
The appsscript.json manifest declares execute-as permissions and web app deployment settings. Deployment uses clasp (Google's CLI tool):
npm install -g @google/clasp
clasp login
cd ~/Documents/repos/sites/86dfrom.com/gas
clasp push
clasp deploy
The .clasp.json file in the gas directory stores the Apps Script project ID, enabling repeat deployments without re-authenticating.
AWS Infrastructure: S3, CloudFront, Route53
Although initially targeted for Vercel, we also deployed to AWS for redundancy and cost optimization on static assets:
S3 Bucket: 86dfrom-com-static
An S3 bucket (us-east-1, standard storage class) holds the compiled static site. Bucket policy restricts direct public access; all traffic is routed through CloudFront (OAC, Origin Access Control, replaces legacy OAI).
# Deploy site to S3
aws s3 cp ~/Documents/repos/sites/86dfrom.com/site/ \
s3://86dfrom-com-static/ \
--recursive \
--cache-control "public, max-age=3600"
# Invalidate CloudFront to refresh edge caches
aws cloudfront create-invalidation \
--distribution-id E1ABC2DEFG3HIJ \
--paths "/*"
CloudFront Distribution: d1a2b3c4d5e6f.cloudfront.net
A CloudFront distribution sits in front of the S3 bucket, serving content from 400+ edge locations globally. Benefits:
- Latency reduction: Users download from the nearest edge location, not origin
- DDoS mitigation: AWS Shield Standard is free; Shield Advanced ($3k/year) optional
- HTTPS enforcement: Integrated ACM certificate (86dfrom.com, 86from.com SANs) at no additional cost
- Cache optimization: Custom TTLs per path (index.html: 300s, static assets: 86400s)
Origin settings use S3 domain 86dfrom-com-static.s3.us-east-1.amazonaws.com with OAC, preventing direct S3 access.
Route53 Hosted Zone: 86dfrom.com
DNS records alias 86dfrom.com and www.86dfrom.com to the CloudFront distribution. A second distribution with a Lambda@Edge redirect function handles the legacy domain 86from.com, redirecting all traffic to the canonical domain with a 301 permanent redirect.
# Redirect function deployed to CloudFront viewer request
function handler(event) {
var request = event.request;
var headers = request.headers;
var host = headers.host.value;
if (host === '86from.com' || host === 'www.86from.com') {
return {
statusCode: 301,
statusDescription: