Building a Printful-Integrated T-Shirt E-Commerce Site with Next.js 14, Stripe, and AWS CloudFront
We recently completed the infrastructure and initial application setup for 86dfrom.com, a print-on-demand t-shirt storefront built on Next.js 14 with Printful fulfillment and Stripe payments. This post details the technical architecture, deployment strategy, and integration patterns we used to ship a production-ready site in a single session.
What Was Built
The project is a modern e-commerce application that:
- Serves static HTML/CSS/JS from CloudFront with S3 origin for the marketing site
- Runs a Next.js 14 API backend (deployed to Vercel) for variant lookups, Stripe webhook handling, and Printful order integration
- Fetches product variant IDs dynamically from Printful's API to display accurate inventory and pricing
- Processes payments via Stripe with webhook validation
- Manages DNS across two domains:
86dfrom.com(primary) and86from.com(redirect) via Route53
The repository structure lives at ~/Documents/repos/sites/86dfrom.com/ with three key directories:
/site— static HTML/CSS served via CloudFront/gas— Google Apps Script configuration (future webhook processing)/scripts— deployment and utility scripts
Infrastructure Architecture
DNS & Domain Management (Route53)
We created separate hosted zones for both domain variations to ensure clean DNS records and flexibility for future subdomains:
- Primary domain:
86dfrom.com— A record pointing to CloudFront distributiond12abc...cloudfront.net - Redirect domain:
86from.com— separate CloudFront distribution with HTTP redirect function, also via Route53
Both required ACM certificate validation. We added DNS CNAME records to Route53 hosted zones for automatic validation, eliminating manual email confirmation steps. Once validated, the certificates became immediately available for CloudFront attachment.
CDN & Static Asset Delivery (CloudFront + S3)
The static site is deployed to S3 bucket 86dfrom-site with the following architecture:
S3 Bucket (86dfrom-site)
├─ /site/index.html (main storefront)
├─ /site/success.html (post-purchase confirmation)
└─ /assets/ (CSS, JS, images)
↓
CloudFront Distribution (d...cloudfront.net)
├─ Origin: S3 bucket
├─ Behaviors: cache HTML (300s TTL), assets (31536000s TTL)
├─ Function (Redirect): 301 for 86from.com traffic
└─ SSL/TLS: ACM certificate for 86dfrom.com
Key caching decisions:
- HTML files: 5-minute TTL (300 seconds) to allow rapid content updates without full invalidation
- Static assets: 1-year TTL (31536000 seconds) with versioned filenames for cache busting
- Invalidation: Post-deploy invalidation of
/index.htmland/success.htmlto ensure fresh content immediately
API & Backend (Vercel + Next.js 14)
The Next.js application handles dynamic logic with these routes:
/api/variants— fetches Printful product variant IDs and pricing/api/checkout— initiates Stripe payment intents/api/webhook— validates and processes Stripe webhook events (payment confirmations, disputes)/api/order— submits orders to Printful after payment confirmation
The application structure:
next.js project root
├─ app/api/variants/route.js
├─ app/api/checkout/route.js
├─ app/api/webhook/route.js
├─ app/api/order/route.js
└─ .env.local (credentials, populated pre-deploy)
All routes were verified with a clean Next.js build before deployment:
$ npm run build
# Output: compiled successfully
Printful Integration
We use the Printful API (base URL: api.printful.com) with token-based authentication. The integration retrieves product variant IDs for Bella+Canvas 3001 Black t-shirts (5 variants, SKUs 4016–4020).
The variant fetching script (/scripts/get-printful-variants.js) runs once to populate the environment file with variant IDs. This script:
- Authenticates to Printful using the API token in
.env.local - Queries the
/api/v2/productsendpoint for the product ID - Filters for Black colorway variants
- Extracts the 5 variant IDs
- Outputs them in format:
VARIANT_IDS=id1,id2,id3,id4,id5
Why separate the variant fetch into a pre-deploy script? Printful variant IDs are stable during the product lifecycle but may change if inventory is restructured. Running this once during setup ensures our environment file is authoritative and reduces runtime API calls to Printful.
Stripe Payment Processing
Payments flow through Stripe with the following architecture:
- Client-side: Frontend collects card details via Stripe Elements (secure, PCI-compliant)
- API:
/api/checkoutcreates a PaymentIntent, returns client secret to frontend - Confirmation: Frontend confirms payment with client secret
- Webhook:
/api/webhooklistens forpayment_intent.succeededevents, submits order to Printful
The webhook endpoint uses raw request body parsing to validate Stripe's signature before processing:
POST /api/webhook
Headers: stripe-signature: t=...,v1=...
Body: raw (unparsed) for signature verification
This pattern prevents replay attacks and ensures only legitimate Stripe events trigger order submission.
Deployment Strategy
Static Site to S3 + CloudFront
The deployment script (/scripts/deploy.sh) handles:
#!/bin/bash
# Sync local /site directory to S3
aws s3 sync ./site s3://86dfrom-site/site \
--delete \
--cache-control "max-age=300"
# Invalidate CloudFront to force refresh