Building a Printful + Stripe T-Shirt Commerce Site: Next.js 14 to AWS CloudFront Infrastructure
This post documents the complete build and deployment of 86dfrom.com, a print-on-demand t-shirt storefront integrating Printful's product catalog with Stripe payment processing. The project demonstrates a modern full-stack architecture spanning Next.js 14 API routes, Google Apps Script webhooks, and AWS infrastructure (S3 + CloudFront + Route53).
What Was Built
86dfrom.com is a single-product t-shirt e-commerce site with the following architecture:
- Frontend: Next.js 14 with server-side rendered product pages and variant selection
- Backend: Next.js API routes handling Printful variant fetching and Stripe payment intent creation
- Fulfillment: Printful integration via REST API for real-time variant data and order fulfillment
- Payments: Stripe payment processing with webhook validation and order confirmation
- Infrastructure: AWS S3 static hosting with CloudFront CDN, Route53 DNS, and ACM SSL certificates
- CRM: Google Apps Script backend for order data capture and email notifications
Project Structure
The repository is organized as follows:
86dfrom.com/
├── site/ # Static HTML for S3 hosting
│ ├── index.html # Product page
│ └── success.html # Order confirmation
├── gas/ # Google Apps Script
│ ├── Code.gs # Webhook handler and Sheets integration
│ ├── appsscript.json # GAS manifest
│ └── .clasp.json # Clasp project config
├── scripts/
│ ├── deploy.sh # S3 + CloudFront deployment script
│ └── get-printful-variants.js # API variant fetcher
├── .env.local # Environment secrets (not in repo)
└── README.md
The initial development was bootstrapped in /Users/cb/Desktop/86dfrom, then migrated to the canonical repo at ~/Documents/repos/sites/86dfrom.com for long-term maintenance.
Printful Integration: Variant Data Pipeline
The core commerce logic centers on fetching product variants from Printful's API. The Bella+Canvas 3001 Black t-shirt is configured with five size variants (XS–2XL), each with a unique Printful product ID.
Rather than hardcoding variant IDs, we built scripts/get-printful-variants.js to dynamically query the Printful API:
node scripts/get-printful-variants.js
This script authenticates via the Printful API key (stored in .env.local) and outputs variant IDs in the format:
XS: 4016
S: 4017
M: 4018
L: 4019
2XL: 4020
These IDs are then embedded in the Next.js API route /api/variants, which serves them to the frontend on page load. This decouples variant management from code changes—if Printful changes product structure, only the script needs re-running.
API Routes and Stripe Payment Flow
Two critical API routes power the checkout experience:
/api/variants— Returns available sizes, prices, and Printful product IDs. Called on page load to populate the size selector./api/create-payment-intent— Accepts a POST withvariantIdandquantity, creates a Stripe PaymentIntent, and returns the client secret. The frontend then uses Stripe.js to display the payment form./api/webhook— Receivespayment_intent.succeededevents from Stripe, validates the signature, and forwards order data to Google Apps Script for fulfillment and CRM logging.
The payment flow is intentionally simple: no shopping cart, no inventory. One variant, one quantity selection, one payment. This minimizes complexity and Stripe fee structure.
Google Apps Script Webhook Handler
Rather than stand up a separate Node backend for order processing, we leverage Google Apps Script (GAS) as a lightweight CRM. The file gas/Code.gs contains two functions:
function doPost(e) {
// Validates webhook signature from /api/webhook
// Logs order data to a Google Sheet
// Sends confirmation email to customer
}
function sendEmailConfirmation(email, orderId, variant, quantity) {
// Composes and sends email via Gmail
}
The appsscript.json manifest declares the script as a web app (executeAs: "ME"), allowing it to receive POST requests at a public URL. The webhook URL is set in the Stripe dashboard under Developers → Webhooks.
Deployment of GAS is handled by clasp (command-line apps script), configured in .clasp.json. This keeps the Google Sheets integration and email logic separate from the main Next.js codebase—a clean separation of concerns.
AWS Infrastructure: S3, CloudFront, and Route53
The static site assets (HTML, CSS, JS) are deployed to an S3 bucket named 86dfrom.com-static. This bucket is configured for web hosting with a bucket policy allowing public reads on all objects:
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::86dfrom.com-static/*"
}
Why not use S3 static website hosting directly? Two reasons:
- CloudFront caching and edge locations: CloudFront caches objects globally, reducing latency for end users. S3 static websites have no built-in CDN.
- SSL/TLS termination at edge: CloudFront distributions can use ACM certificates to serve HTTPS with custom domains. S3 static websites require routing through CloudFront anyway if you want a custom domain and HTTPS.
The CloudFront distribution for 86dfrom.com is configured as follows:
- Origin:
86dfrom.com-static.s3.us-west-2.amazonaws.com - Domain name:
d[random-string].cloudfront.net(auto-generated) - ACM certificate: Covers both
86dfrom.comand*.86dfrom.com, validated via Route53 DNS CNAME records - Cache behaviors: Default TTL 86400 seconds (1 day), max TTL 31536000 (1 year)
- Viewer protocol policy: Redirect HTTP to HTTPS
A second CloudFront distribution was created for the subdomain 86from.com (typo handling). This distribution uses a CloudFront Function (inline JavaScript) to permanently redirect all