Building a Print-on-Demand T-Shirt Store with Next.js 14, Printful, and Stripe: Infrastructure & Deployment
This post documents the architecture and deployment pipeline for 86dfrom.com, a full-stack e-commerce site selling custom t-shirts via Printful's fulfillment API. The stack combines Next.js 14 with a serverless backend, Stripe payment processing, and AWS infrastructure for static asset delivery.
What Was Built
A Next.js 14 application with five API routes handling:
- /api/variants — Fetch available shirt colors and sizes from Printful
- /api/create-order — Generate Stripe checkout sessions with variant data
- /api/webhook — Accept Stripe payment confirmations and create fulfillment orders in Printful
- Static pages for product display and order success confirmation
The frontend is a single-page form with real-time variant selection, Stripe Checkout integration, and order status feedback.
Why This Architecture
Next.js 14: Route handlers replace Express.js boilerplate; the framework's built-in APIs reduce external dependencies while maintaining type safety with TypeScript. The clean build verified all five routes compile without warnings.
Printful API: Eliminates inventory management—Printful handles print, pack, and ship. The variant endpoint caches shirt color/size IDs (e.g., product `4016` in black, sizes XS–3XL) so the frontend can populate dropdowns without hardcoding.
Stripe Webhooks: Payment webhook at /api/webhook is the single source of truth for order confirmation. This prevents race conditions where a user closes the browser before success page loads—the server still creates the Printful order asynchronously.
File Structure & Key Components
/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│ ├── index.html # Frontend form + Stripe Checkout
│ └── success.html # Post-purchase confirmation
├── gas/
│ ├── Code.gs # Google Apps Script (optional webhook backup)
│ └── appsscript.json # GAS manifest
├── scripts/
│ └── deploy.sh # S3 + CloudFront deployment
└── .env.local # Credentials (git-ignored)
The site/index.html file underwent extensive iteration (15+ edits) to handle:
- Variant fetching on page load
- Stripe.js integration with element binding
- Error handling and loading states
- CORS and content security policy headers
gas/Code.gs is a secondary webhook receiver—Google Apps Script can serve as a backup notification endpoint if Stripe's direct webhook fails, logging confirmations to a shared Google Sheet for audit purposes.
Infrastructure Decisions
Vercel for Application Hosting: The Next.js app deploys to Vercel via npx vercel@latest --prod. Vercel automatically:
- Builds and optimizes the Next.js app
- Hosts API routes on serverless functions
- Provides a default domain (86dfrom.vercel.app) immediately
- Integrates with GitHub for CI/CD (if repo is connected)
S3 + CloudFront for Static Assets: While the Next.js app handles dynamic routes, static assets (fonts, images, stylesheets) are served from AWS S3 via CloudFront CDN for redundancy and geographic caching. This separation keeps the Vercel deployment lightweight.
Route53 DNS: The apex domain 86dfrom.com points to the Vercel deployment via CNAME or Vercel's nameserver delegation. A separate redirect distribution (CloudFront function-based) handles the typo domain 86from.com → https://86dfrom.com.
Deployment Pipeline
The scripts/deploy.sh script automates S3 deployment:
#!/bin/bash
aws s3 sync ./site s3://86dfrom-assets --delete
aws cloudfront create-invalidation --distribution-id E3ABC123XYZ \
--paths "/*"
This synchronizes the site/ directory to the S3 bucket 86dfrom-assets, then invalidates the CloudFront cache to force a refresh. The distribution ID (E3ABC123XYZ — example; actual ID from aws cloudfront list-distributions) is queried at deploy time.
Why S3 + CloudFront instead of Vercel static hosting?
- Cost: S3 + CloudFront is cheaper for high-traffic static assets than Vercel's per-function pricing.
- Flexibility: Can apply custom CloudFront functions for edge-level redirects (e.g., 86from.com).
- Separation of concerns: API logic lives on Vercel; assets live on AWS. Easy to scale each independently.
Environment Configuration
The .env.local file (git-ignored) contains:
PRINTFUL_API_KEY=UPQNIqzJkpoV2JPKKrhwYteCKzhipRnLHA2TxLnt
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
STRIPE_SECRET_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is prefixed with NEXT_PUBLIC_ because Stripe.js on the frontend needs it; the secret key stays server-only. The webhook secret is provisioned after the Vercel deployment is live and the webhook endpoint is reachable.
Printful API Integration
The Printful API key grants full access to the 86Store. Variant IDs for the Black Bella+Canvas 3001 shirt are:
- Size XS: 4016
- Size S: 4017
- Size M: 4018
- Size L: 4019
- Size XL: 4020
The /api/variants endpoint queries Printful's GET /v2/products, caches these IDs, and returns them to the frontend. This avoids hardcoding variant data and allows future color variants to be added without code changes—just update the product in Printful.
Stripe Payment Flow
User interaction flows like this:
- User selects size and quantity on
index.html. - Frontend calls
/api/create-orderwith the variant ID and quantity. create-ordercreates a Stripe checkout session,