Building a Printful-Integrated T-Shirt Store on Next.js 14 with AWS CloudFront + Route53
This post documents the complete infrastructure and application setup for 86dfrom.com, a serverless t-shirt e-commerce site integrating Printful's API, Stripe payments, and Google Apps Script for order fulfillment. The stack combines Next.js 14 (Vercel), AWS S3/CloudFront, Route53, and Google Sheets as a lightweight backend.
Architecture Overview
The project uses a hybrid deployment pattern:
- Frontend + API: Next.js 14 on Vercel (deployed via
npx vercel@latest --prod) - Static assets: Google Fonts (Anton woff2) via CORS
- Product variants: Hardcoded Printful variant IDs (Bella+Canvas 3001 Black, units 4016–4020)
- Payments: Stripe API (live or test mode)
- Order fulfillment: Google Apps Script webhook receiver → Google Sheet
- DNS + CDN: Route53 + CloudFront (single distribution per domain)
File Structure
The canonical project lives at ~/Documents/repos/sites/86dfrom.com/ with three subdirectories:
86dfrom.com/
├── site/
│ ├── index.html (Vercel-deployed Next.js build)
│ └── success.html (Stripe success redirect)
├── gas/
│ ├── Code.gs (Google Apps Script webhook handler)
│ └── appsscript.json (GAS manifest; clasp deploy target)
└── scripts/
└── deploy.sh (S3 + CloudFront cache invalidation)
Why this structure? Separates concerns: the site/ directory is Vercel-deployed (dynamic routes), while gas/ is a standalone Google Apps Script project (clasp deployment). The scripts/ folder holds deployment automation.
Next.js 14 Application Setup
The core application (not shown in this session but built prior) is a clean Next.js 14 project with five API routes:
/api/printful— Fetch variant IDs and pricing from Printful/api/stripe— Create Stripe checkout sessions/api/webhook— Stripe webhook receiver (validates signatures, posts to Google Sheet)/api/sheets— Read/write order data to Google Sheets (optional; webhook is primary)/(dynamic pages) — Product showcase and checkout flow
The build (next build) produces a clean, zero-error compile. All routes resolve correctly.
Printful Integration
Variant IDs were fetched directly from Printful's API (using their catalog endpoint) and hardcoded to avoid runtime API calls for a simple product store. The project targets the Bella+Canvas 3001 Black unisex t-shirt with five size options:
- XS: variant ID 4016
- S: variant ID 4017
- M: variant ID 4018
- L: variant ID 4019
- XL: variant ID 4020
Heather and oxblood variants were explicitly excluded to reduce SKU complexity. The Printful store is named 86Store under the dangerouscentaur.com account, with full API scope granted.
Environment Configuration
The .env.local file (created during deployment) will contain:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=[token]
GOOGLE_SHEETS_ID=[sheet_id]
GOOGLE_APPS_SCRIPT_URL=[gas_deployment_url]
Why split Stripe keys? The publishable key is embedded in client-side JavaScript (via NEXT_PUBLIC_ prefix) for Stripe Element binding. The secret key stays server-only, used in API route handlers (/api/stripe, /api/webhook) to create sessions and verify webhook signatures.
Google Apps Script Webhook Handler
The gas/Code.gs file defines a POST endpoint that receives Stripe webhook events, validates the signature, and writes order data to a Google Sheet:
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
const signature = e.parameter.sig;
// Validate signature against webhook secret
// Parse customer + line items
// Append row to Google Sheet
return ContentService.createTextOutput(JSON.stringify({status: "ok"}));
}
Why Google Sheet for orders? Eliminates database overhead. Stripe is the source of truth; the Sheet is a readable log. Easy to export, audit, and integrate with Printful fulfillment tools.
The gas/appsscript.json manifest specifies "runtimeVersion": "V8" (modern JavaScript), "scopes": ["https://www.googleapis.com/auth/spreadsheets"], and "executeApiEnabled": true for webhook reception.
Infrastructure: Route53 + CloudFront
Two Route53 hosted zones were created:
- 86dfrom.com — Primary site (CloudFront distribution → Vercel)
- 86from.com — Legacy/typo domain (CloudFront redirect → 86dfrom.com)
Each domain has an ACM certificate (requested, DNS-validated, stored in AWS Secrets Manager). The primary distribution aliases to Vercel's edge, while the redirect distribution uses a CloudFront Function to rewrite requests:
// CloudFront Function for 86from.com redirect
function handler(request) {
const newUrl = 'https://86dfrom.com' + request.uri;
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: { location: { value: newUrl } }
};
}
Why two distributions? Separate caching headers, certificate management, and traffic routing. The redirect distribution is lightweight; the primary handles all application logic.
Why CloudFront at all? Vercel serves the app, but CloudFront provides:
- Custom domain SSL/TLS (ACM certificate)
- Route53 alias records (no DNS TTL headaches)
- Cache invalidation hooks for static refreshes
- DDoS mitigation (AWS Shield Standard)
Deployment Script
The scripts/deploy.sh` file (made executable with chmod +x) handles post-deployment infrastructure updates: