Building a Print-on-Demand T-Shirt Store with Next.js 14, Printful, and Stripe: Infrastructure & Deployment
This post documents the full-stack deployment of 86dfrom.com, a dynamic print-on-demand t-shirt storefront built on Next.js 14, integrated with Printful's API for inventory management and Stripe for payment processing. We'll cover the architecture decisions, infrastructure setup, and deployment pipeline that tie everything together.
Architecture Overview
The application follows a modern serverless-first pattern:
- Frontend: Next.js 14 with React Server Components, deployed to Vercel
- Inventory/Fulfillment: Printful API integration for variant management and order routing
- Payments: Stripe for payment collection and webhook event handling
- DNS & CDN: Route53 and CloudFront for global distribution and DNS routing
- Static Assets: Google Fonts (Anton) cached via CloudFront
Project Structure
The source repository lives at /Users/cb/Documents/repos/sites/86dfrom.com/ with a clean separation of concerns:
86dfrom.com/
├── site/ # Static HTML fallback (success page, etc.)
│ ├── index.html # Main landing page
│ └── success.html # Post-purchase confirmation
├── gas/ # Google Apps Script (unused in this build, kept for reference)
│ ├── Code.gs
│ └── appsscript.json
├── scripts/
│ ├── deploy.sh # S3 + CloudFront deployment automation
│ └── get-printful-variants.js # Variant ID extraction
└── .env.local # Environment secrets (NOT committed)
The Next.js application (compiled version) is deployed directly to Vercel, bypassing the static site in most cases. The static site serves as a fallback and houses the success page that Stripe redirects to post-purchase.
Printful API Integration
To populate variant IDs for the Bella+Canvas 3001 Black t-shirt, we created a script at scripts/get-printful-variants.js that queries the Printful API:
node scripts/get-printful-variants.js
This script:
- Authenticates to Printful using the API key (stored in
.env.localasPRINTFUL_API_KEY) - Queries the
/api/productsendpoint scoped to the 86Store - Filters for the Bella+Canvas 3001 product in Black colorway
- Extracts variant IDs for XS–3XL sizes (typically IDs 4016–4020)
- Outputs JSON mapping size → variant ID for hardcoding into the application
Why hardcode variants? The Printful API requires authentication on every request. For a stateless, edge-deployed Next.js app on Vercel, hardcoding variant mappings avoids unnecessary latency and credential exposure in runtime requests. Variants only change when the product catalog changes, which is rare.
Environment Configuration
The .env.local file (never committed to git) contains:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
PRINTFUL_API_KEY=UPQNIqzJkpoV2JPKKrhwYteCKzhipRnLHA2TxLnt
PRINTFUL_STORE_ID=86Store
NEXT_PUBLIC_* variables are exposed to the browser (required for Stripe.js client initialization); others remain server-only. This separation follows Vercel's security best practices.
API Routes & Webhook Handling
The application defines two core API routes:
/api/create-checkout-session— POST endpoint that accepts cart data (size, quantity), calls Stripe to create a Checkout Session, and returns the session ID to redirect the client tostripe.com/pay/{session_id}/api/webhook— POST endpoint that validates incoming webhook signatures from Stripe (usingSTRIPE_WEBHOOK_SECRET), parsespayment_intent.succeededevents, and triggers order fulfillment via Printful's order creation API
The webhook signature validation uses HMAC-SHA256, ensuring requests are genuinely from Stripe's servers and haven't been tampered with in transit.
DNS & CloudFront Setup
We configured two distinct DNS patterns for 86dfrom.com and a catchall domain 86from.com:
Primary Domain: 86dfrom.com
- Route53 Hosted Zone: Created in us-east-1 for DNS management
- ACM Certificate: Issued for
86dfrom.comwith DNS validation via Route53 CNAME records - CloudFront Distribution: Origin points to Vercel's edge network
- Viewer protocol policy: Redirect HTTP → HTTPS
- Allowed HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE (to allow Stripe webhooks)
- Cache behavior: Forward all headers and query strings to origin (Vercel handles caching)
- Route53 A Record: Alias record pointing distribution domain to CloudFront
Redirect Domain: 86from.com
- ACM Certificate: Separate cert for the typo-prone domain
- CloudFront Distribution: Uses CloudFront Functions to redirect all requests to
https://86dfrom.com- Function deployed at viewer-request phase
- Returns 301 permanent redirect to standardize traffic on primary domain
- Route53 A Record: Alias to redirect distribution
Why separate distributions? CloudFront Functions have different pricing and capabilities than Lambda@Edge. For a simple HTTP redirect, a CloudFront Function is lighter-weight and cheaper. For the primary domain serving dynamic content, we let CloudFront proxy to Vercel's origin.
Build & Deployment Pipeline
The deployment follows this sequence:
- Local build:
npm run buildin the Next.js app directory compiles all routes and verifies no TypeScript/ESLint errors - Vercel deployment:
npx vercel@latest --produploads the build artifact and publishes to production - Environment configuration: Vercel project settings receive the three secrets (
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,PRINTFUL_API_KEY) and one public variable (NEXT_PUBLIC_STRIPE_