Building a Printful-Integrated T-Shirt Storefront: Next.js 14, Stripe, and AWS Infrastructure
This post covers the complete setup of 86dfrom.com, a print-on-demand t-shirt e-commerce site built with Next.js 14, integrated with Printful for inventory management, Stripe for payments, and deployed across Vercel + AWS CloudFront/S3. We'll walk through the architecture decisions, infrastructure setup, and deployment pipeline.
Project Architecture Overview
The site is structured as a monorepo with three distinct deployment targets:
- Next.js frontend + API routes → Vercel (primary application)
- Static HTML fallback → S3 + CloudFront (disaster recovery, CDN caching)
- Google Apps Script webhooks → GAS deployment (order notifications)
This separation allows us to handle dynamic commerce logic (Stripe webhooks, Printful sync) via Vercel's serverless functions while maintaining a static CDN-backed fallback for high-traffic scenarios.
File Structure and Source Organization
The project lives in two locations during development and production:
~/Desktop/86dfrom/ # Development workspace
├── site/
│ ├── index.html # Main product page
│ └── success.html # Post-checkout confirmation
├── gas/
│ ├── Code.gs # Google Apps Script backend
│ └── appsscript.json # GAS manifest (scopes, version)
├── scripts/
│ ├── deploy.sh # S3 + CloudFront invalidation
│ └── get-printful-variants.js # Variant ID fetcher
└── .env.local # Secrets (Stripe, Printful)
~/Documents/repos/sites/86dfrom.com/ # Git source of truth
└── [mirror of above]
Both directories are synced via cp -r during the dev session; the repo directory becomes the canonical source for version control.
Printful Integration and Variant Management
Printful hosts the actual inventory and fulfillment. The site needs to reference specific product variants (SKUs) to process orders correctly.
Variant IDs used: 4016, 4017, 4018, 4019, 4020 (Bella+Canvas 3001 Black, various sizes). These are fetched via:
node scripts/get-printful-variants.js
The script calls the Printful API endpoint with an API token stored in .env.local under NEXT_PUBLIC_PRINTFUL_API_KEY, filters by product name and color, and outputs variant objects containing:
- Variant ID (used in order creation)
- Size
- Price
- Available inventory status
These values populate the frontend dropdown and are passed to the Stripe checkout session metadata, then to Printful's order creation endpoint when webhooks fire.
Environment Configuration Strategy
The .env.local file contains three categories of credentials:
- Printful API key — Used by
scripts/get-printful-variants.jsand GAS Code.gs for order sync - Stripe secret key — Used by
/api/webhookto verify webhook signatures and create orders - Stripe publishable key — Embedded in frontend HTML for client-side Checkout initialization
This file is .gitignore'd in the repo but must be manually created before each deployment. For Vercel deployments, these same values are added to the project settings at vercel.com → 86dfrom → Settings → Environment Variables.
Infrastructure: S3 + CloudFront + Route53
S3 Bucket Setup
Created S3 bucket 86dfrom.com (region: us-east-1) with:
- Static website hosting enabled (index:
index.html) - Bucket policy allowing CloudFront OAI (Origin Access Identity) to read all objects
- CORS headers configured for font and image assets
# Deploy via:
aws s3 sync ~/Documents/repos/sites/86dfrom.com/site/ \
s3://86dfrom.com --delete
# Invalidate CloudFront cache:
aws cloudfront create-invalidation \
--distribution-id E1A2B3C4D5E6F7 \
--paths "/*"
CloudFront Distributions
Primary distribution (86dfrom.com):
- Origin: S3 bucket with OAI (not public website endpoint)
- Default root object:
index.html - Cache behaviors:
- Static assets (CSS, JS, fonts): 30-day TTL
- HTML: 5-minute TTL (allows quick fixes without full invalidation)
- Viewer protocol: Redirect HTTP → HTTPS
- Custom domain:
86dfrom.com - SSL certificate: ACM-managed, auto-renewal
Redirect distribution (86from.com):
A second distribution handles the typo domain (86from.com without "d"). It uses a CloudFront function (Lambda@Edge alternative) to 301-redirect all traffic to the primary domain. This prevents user confusion and consolidates SEO equity.
// Redirect function deployed to 86from.com distribution
function handler(event) {
var request = event.request;
var host = request.headers.host.value;
if (host === '86from.com') {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
'location': { value: 'https://86dfrom.com' + request.uri }
}
};
}
return request;
}
Route53 DNS Records
Two hosted zones were configured:
86dfrom.comzone:A record→ CloudFront distribution alias (primary site)AAAA record→ CloudFront IPv6 aliasCNAMEfor ACM certificate validation (temporary, auto-removed post-validation)
86from.comzone:A record→ Redirect CloudFront distribution aliasAAAA record→ Redirect CloudFront IPv6 alias
Vercel Deployment and API Routes
The Next.js application is deployed to