Building a Print-on-Demand T-Shirt Site: Next.js, Printful, Stripe, and AWS Infrastructure
This post documents the full-stack setup of 86dfrom.com, a Stripe-enabled print-on-demand merchandise site powered by Next.js 14, Printful API integration, and AWS CloudFront + S3 distribution. The project demonstrates a production-ready pattern for combining serverless backend (Vercel), third-party fulfillment APIs, and payment processing at scale.
Project Architecture Overview
The 86dfrom.com site is built as a Next.js 14 application with the following architecture:
- Frontend: React/Next.js 14 running on Vercel (auto-scaling serverless)
- Backend: Next.js API routes handling Printful product sync, Stripe checkout, and webhook ingestion
- Fulfillment: Printful API for inventory, variants, and order fulfillment
- Payments: Stripe for checkout and subscription webhooks
- Static Mirror: S3 + CloudFront serving static HTML for 86from.com (domain redirect variant)
- DNS: Route53 managing both 86dfrom.com and 86from.com zones
File Structure and Component Organization
The project is organized under /Users/cb/Documents/repos/sites/86dfrom.com/ with the following layout:
86dfrom.com/
├── site/
│ ├── index.html # Static fallback page
│ └── success.html # Order confirmation page
├── gas/
│ ├── Code.gs # Google Apps Script (if needed for sheets integration)
│ └── appsscript.json # GAS manifest
├── scripts/
│ ├── deploy.sh # S3 + CloudFront deployment script
│ └── get-printful-variants.js # Extracts variant IDs from Printful API
├── .env.local # Local secrets (Stripe, Printful keys)
└── [Next.js app root] # Standard Next.js 14 structure
├── app/
│ ├── api/
│ │ ├── checkout/route.ts # POST endpoint for Stripe session creation
│ │ ├── variants/route.ts # GET endpoint for product variants
│ │ └── webhook/route.ts # POST endpoint for Stripe events
│ ├── success/page.tsx # Order confirmation page
│ └── page.tsx # Homepage with product grid
├── public/
│ └── [product images, fonts]
└── package.json
Printful API Integration
The Printful integration is handled through two key components:
Variant ID Extraction: The script scripts/get-printful-variants.js queries the Printful API to fetch available product variants for the store "86Store". The script:
- Authenticates using the Printful API key (bearer token in Authorization header)
- Calls
GET /api/v2/storesto confirm store access - Iterates through product variants, filtering for the Bella+Canvas 3001 Black t-shirt (style ID 21)
- Extracts variant IDs for sizes XS–3XL (variant IDs 4016–4020)
- Outputs JSON mapping:
{ "XS": 4016, "S": 4017, "M": 4018, "L": 4019, "XL": 4020 }
These variant IDs are then stored in .env.local as:
NEXT_PUBLIC_PRINTFUL_VARIANTS_DARK_TEE={"XS":4016,"S":4017,"M":4018,"L":4019,"XL":4020}
PRINTFUL_API_KEY=[api_key_here]
PRINTFUL_STORE_ID=86Store
Backend Variant Route: The API endpoint app/api/variants/route.ts (GET) returns the parsed variant object to the frontend, allowing the React component to render size selectors dynamically without hardcoding IDs.
Stripe Payment Flow
Payment processing follows a standard Stripe checkout pattern:
- Frontend: User selects size and quantity, clicks "Buy Now"
- POST to /api/checkout: Sends
{ size, quantity, priceInCents } - Backend creates Stripe session: Calls
stripe.checkout.sessions.create()with:- Line items: product metadata (size, variant ID)
- Success URL:
https://86dfrom.com/success - Cancel URL:
https://86dfrom.com - Payment method types:
["card"]
- Redirect to Stripe-hosted checkout: Browser redirected to
stripe.com/pay/[session_id] - Webhook ingestion: POST to
/api/webhookoncharge.succeededevent- Verifies signature using Stripe webhook secret
- Extracts order metadata (size, variant) from session
- Submits order to Printful via
POST /api/v2/orders - Logs confirmation or stores in database (future enhancement)
Environment variables required:
STRIPE_SECRET_KEY=sk_live_[your_key_here]
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_[your_key_here]
STRIPE_WEBHOOK_SECRET=whsec_[generated_after_deploy]
AWS Infrastructure and Static Distribution
A secondary static site is hosted on S3 + CloudFront to serve 86from.com (single-letter variant domain) as a redirect to the main Vercel app. This pattern provides:
- SEO isolation: Separate domain prevents duplicate content penalties
- Zero-latency redirects: CloudFront edge locations serve 301 redirects globally
- Lower cost: Static redirect costs ~$0.01/month vs. spinning a full server
Infrastructure created:
- S3 bucket:
86from.com(public access, static hosting enabled) - ACM certificate:
86from.com(DNS validation via Route53) - CloudFront distribution: ID
[cf_dist_id]- Origin: S3 bucket
86from.com.s3.us-east-1
- Origin: S3 bucket