Building a Print-on-Demand T-Shirt Store: Next.js 14 + Printful + Stripe on AWS Infrastructure
This post documents the complete engineering setup for 86dfrom.com, a Stripe-powered print-on-demand t-shirt storefront integrated with Printful's API. We'll cover the full stack: Next.js 14 on Vercel, Printful variant management, Stripe payment processing, and AWS infrastructure for DNS/CDN routing.
Architecture Overview
The 86dfrom project uses a hybrid deployment model:
- Frontend + API: Next.js 14 on Vercel (production at
/Users/cb/Documents/repos/sites/86dfrom.com) - Payment Processing: Stripe live mode integration with webhook handling
- Print Fulfillment: Printful API for dynamic variant management
- DNS/CDN: Route53 (hosted zone) + CloudFront distributions for static routing and redirects
- Domain Variants: Primary
86dfrom.com+ redirect from86from.com(typo-squat protection)
Project Structure
The codebase is organized as follows:
~/Documents/repos/sites/86dfrom.com/
├── site/
│ ├── index.html # Landing page
│ └── success.html # Post-purchase confirmation
├── gas/
│ ├── Code.gs # Google Apps Script (unused in current flow)
│ ├── appsscript.json # GAS manifest
│ └── .clasp.json # Clasp configuration
├── scripts/
│ └── deploy.sh # Manual deployment script
└── .env.local # Runtime environment variables (git-ignored)
The Next.js application itself (not shown here) lives in the Vercel-connected repo and includes:
/pages/api/variants— Fetch Printful variant IDs for the Black Bella+Canvas 3001/pages/api/checkout— Create Stripe Checkout sessions/pages/api/webhook— Stripe webhook endpoint for order fulfillment/pages/index.js— Product display + checkout button
Printful Integration: Variant Management
Printful's catalog is massive—thousands of products and variants across different colors, sizes, and print positions. For 86dfrom, we're focusing on a single product: Bella+Canvas 3001 Black t-shirt.
The scripts/get-printful-variants.js script (run once, results hardcoded into .env.local) queries Printful's REST API to retrieve variant IDs. Why hardcode? Because variant IDs are static and rarely change. Dynamic lookups on every page load would be wasteful.
Key variant IDs extracted:
Black / XS: 4016
Black / S: 4017
Black / M: 4018
Black / L: 4019
Black / XL: 4020
Why not heather or oxblood variants? We made a deliberate choice to start with the most reliable, fastest-shipping base color. Printful's data shows black Bella+Canvas 3001 has the best lead times and lowest defect rates. We can expand to additional colors later without architectural changes—just update the variant IDs in .env.local and re-deploy.
Stripe Payment Flow
The payment architecture follows PCI-compliance best practices:
- Frontend calls
/api/checkoutwith product selection (size, design) and customer email - Backend creates a Stripe Checkout session, embedding our product name and Printful variant ID
- User redirects to Stripe's hosted checkout (no card data touches our servers)
- Post-purchase, Stripe sends a webhook to
/api/webhookwith the completedpayment_intent.succeededevent - Our webhook handler extracts the order details and forwards them to Printful's order creation API
The webhook secret is stored in Vercel's environment variables (not in .env.local) and rotated periodically. This separation ensures that even if .env.local is accidentally committed, payment processing credentials remain secure.
AWS Infrastructure: DNS and CDN
While Vercel handles the application, we use AWS for domain management and typo-squat protection:
Route53 Hosted Zone
A Route53 hosted zone was created for 86dfrom.com with the following records:
86dfrom.com A→ CloudFront distribution CNAMEwww.86dfrom.com CNAME→ CloudFront distribution domain- ACM validation CNAMEs (temporary, auto-validated)
Why Route53 instead of the registrar's DNS? Programmatic control, integrated CloudFront aliasing, and consistency with other QDN infrastructure (chuckladd.com, dangerouscentaur.com follow the same pattern).
CloudFront Distributions
Primary Distribution (86dfrom.com → Vercel):
- Origin: Vercel's CNAME (e.g.,
86dfrom-production.vercel.app) - Behavior: Forward Host header, allow GET/HEAD/POST/PUT/DELETE (for API routes)
- TLS: ACM certificate auto-issued for
86dfrom.com, validated via Route53 DNS - Cache: 0s default TTL for dynamic API routes, 86400s for static assets
Redirect Distribution (86from.com → 86dfrom.com):
- Origin: S3 redirect bucket (minimal, just hosts a CloudFront Function)
- CloudFront Function: Custom JavaScript that redirects
86from.com/*tohttps://86dfrom.com/*with 301 status code - Purpose: Capture typos and boost SEO authority into the canonical domain
The redirect function is defined in-line as a CloudFront Function (not a Lambda@Edge function) because it's stateless, doesn't require external API calls, and executes in under 1ms. Here's the pattern (no code, just approach):
- Check if request hostname is
86from.com - If yes, construct a 301 redirect to
86dfrom.com+ original path + query string - Return the redirect response immediately
Environment Configuration
The .env.local file (git-ignored, exists only on CI/CD and local dev) contains:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_KEY=UPQNI