Building a Print-on-Demand T-Shirt Store with Next.js 14, Printful, and Stripe: Infrastructure & Deployment Strategy
Over the past development session, we built out the complete infrastructure and deployment pipeline for 86dfrom.com, a print-on-demand t-shirt store powered by Next.js 14, Printful's API, and Stripe payments. This post details the technical decisions, infrastructure setup, and deployment patterns we implemented.
Project Architecture Overview
The project structure follows a clean separation of concerns:
- Frontend: Next.js 14 with 5 API routes (product catalog, variant details, checkout, webhook receiver, order status)
- Backend Services: Printful API for inventory/fulfillment, Stripe for payment processing
- Infrastructure: Vercel for hosting, Route53 for DNS, CloudFront for static asset delivery, S3 for origin storage
- Automation: Bash deployment scripts, Google Apps Script for order notifications
The build pipeline was validated clean—all 5 routes compile without errors, confirming the Next.js 14 configuration is production-ready.
Printful API Integration & Variant Management
Rather than hardcoding product variant IDs, we created a programmatic approach to fetch and cache them. The Printful API requires:
- Store ID: Retrieved from the Printful dashboard under
Settings → Stores - API Token: Generated at
Settings → APIwith full scope across all stores
The workflow involves:
- Running
scripts/get-printful-variants.jsto query the Printful API and extract variant IDs for a specific product (in this case, Bella+Canvas 3001 Black t-shirt) - Storing these IDs in
.env.localas comma-separated values for reference in API routes - Using those IDs when constructing POST requests to Printful's order creation endpoint
This approach decouples product definitions from code deployment—if Printful changes variant IDs, you update the script output and redeploy environment variables without touching source code.
Environment Configuration & Secrets Management
We follow a strict separation of development and production secrets:
.env.local(development): Contains test API keys, Printful token, test Stripe secret- Vercel Environment Variables: Production keys, synced via the Vercel CLI during deploy
- Google Cloud Secrets (optional): For sensitive values shared across multiple services
The .env.local file structure includes:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_TOKEN=...
PRINTFUL_STORE_ID=...
PRINTFUL_PRODUCT_VARIANT_IDS=4016,4017,4018,4019,4020
WEBHOOK_SECRET=whsec_...
Note: NEXT_PUBLIC_ prefix is used only for the Stripe publishable key, which is safe to expose client-side. All secret keys remain server-side only.
DNS & CloudFront Distribution Architecture
The domain 86dfrom.com required multi-step AWS setup:
Route53 Hosted Zone
Created a new hosted zone for 86dfrom.com in Route53. This gave us authoritative nameservers to configure at the domain registrar.
ACM Certificate
Requested an ACM certificate for both 86dfrom.com and www.86dfrom.com (wildcard not used to avoid including 86from.com). The certificate required DNS validation:
- ACM provided CNAME records (typically
_xxxxx.86dfrom.com → _yyyyy.acm-validations.aws) - We added these CNAME records directly in Route53 via CLI
- AWS validated ownership within minutes
CloudFront Distribution (Primary)
Created the main distribution (86dfrom.com) with:
- Origin: Vercel deployment (auto-detected as HTTP origin, CloudFront handles HTTPS to origin)
- Alternate domain names:
86dfrom.com,www.86dfrom.com - SSL certificate: The ACM certificate from above
- Cache behaviors: Short TTLs (60s) for dynamic content (
/api/*), longer TTLs (3600s) for static assets - Origin request policy: Forward all headers to Vercel (required for Next.js cookie-based sessions)
Route53 A records alias to the CloudFront distribution:
86dfrom.com → CloudFront dist ID (A record, alias)
www.86dfrom.com → CloudFront dist ID (A record, alias)
CloudFront Redirect Distribution (Secondary)
A separate distribution was created to handle 86from.com (typo domain) with a CloudFront function that redirects all traffic to 86dfrom.com:
- Function type: CloudFront Functions (viewer request)
- Logic: Returns HTTP 301 redirect with
Location: https://86dfrom.com - Rationale: Cheap redirect layer (Functions are free, unlike Lambda@Edge); no backend involved
This pattern avoids paying for traffic through a backend and prevents search engine confusion about canonical domain.
Deployment Pipeline
The deployment strategy uses a two-stage approach:
Stage 1: Vercel Production Deploy
Command:
npx vercel@latest --prod
This:
- Builds the Next.js 14 project in Vercel's environment
- Runs the
buildscript frompackage.json - Detects all 5 API routes and deploys them as serverless functions
- Assigns a temporary Vercel URL (
86dfrom-xxx.vercel.app) as the origin for CloudFront
Environment variables are set via Vercel CLI before deploy or added manually in the Vercel dashboard under Settings → Environment Variables.
Stage 2: CloudFront Cache Invalidation
After deploy, any edge-cached routes must be invalidated:
aws cloudfront create-invalidation \
--distribution-id E1234ABCD5678 \
--paths "/*"
This clears CloudFront's cache, forcing edge nodes to fetch fresh content from Vercel on next request.