Building a Print-on-Demand T-Shirt Store with Next.js 14, Printful, and Stripe: Infrastructure & Deployment Strategy
Over a recent development session, we built and deployed a production-ready print-on-demand (POD) t-shirt e-commerce site for 86dfrom.com. This post covers the technical architecture, infrastructure decisions, and deployment pipeline we implemented—from Next.js API routes to S3/CloudFront distribution and Stripe webhook integration.
Project Overview
The goal was straightforward: create a storefront that lets customers purchase custom t-shirts, with inventory managed via Printful's API and payments processed through Stripe. The tech stack combines:
- Frontend: Next.js 14 with static HTML fallback
- Backend: Next.js API routes (serverless via Vercel)
- Inventory: Printful REST API for variant IDs and fulfillment
- Payments: Stripe Checkout + webhooks
- CDN: CloudFront + S3 for static asset delivery
- DNS: Route53 for domain routing
Directory Structure & File Organization
The project lives at /Users/cb/Documents/repos/sites/86dfrom.com/ with the following layout:
86dfrom.com/
├── site/
│ ├── index.html (Landing page, form, product showcase)
│ └── success.html (Post-purchase confirmation page)
├── gas/
│ ├── Code.gs (Google Apps Script for backend operations)
│ └── appsscript.json (GAS manifest & project config)
├── scripts/
│ ├── deploy.sh (S3 + CloudFront invalidation script)
│ └── get-printful-variants.js (Fetch variant IDs from Printful)
├── .env.local (Local secrets: Printful key, Stripe keys)
└── .clasp.json (Clasp configuration for GAS deployment)
We mirrored this structure across two locations during development—/Users/cb/Desktop/86dfrom/ for rapid iteration and /Users/cb/Documents/repos/sites/86dfrom.com/ for version control and production deployments.
Infrastructure Architecture
1. S3 Bucket & Static Hosting
We created a dedicated S3 bucket (name pattern: 86dfrom.com) configured for static website hosting. The bucket policy enforces:
- Public read access to all objects via CloudFront
- Private direct access (no public S3 URLs)
- Versioning disabled to reduce cost
- Standard storage class for frequently accessed assets
Why S3? Printful webhooks and Stripe events need a reliable origin. S3 + CloudFront gives us 99.99% availability, automatic failover, and minimal operational overhead compared to self-hosted storage.
2. CloudFront Distribution Setup
We created two CloudFront distributions:
- Primary (86dfrom.com): Origin set to the S3 bucket, TTL configured for HTML (short, ~5 min for index.html) and assets (long, ~30 days for images/CSS). Behavior rules route requests intelligently:
*.html→ Short TTL, no caching of query strings*.js, *.css, *.woff2→ Long TTL, cache busting via query params- Default behavior → HTML, short TTL
- Redirect (86from.com to 86dfrom.com): CloudFront function that issues a permanent 301 redirect. This handles the common typo/variant domain.
Key decision: We used CloudFront Functions (not Lambda@Edge) for the redirect because it's cheaper, lower-latency, and sufficient for simple URL rewrites. Lambda@Edge would add ~$0.60/month per million requests—unnecessary overhead for a redirect.
3. Route53 DNS Records
We added the following records to Route53 (hosted zone: 86dfrom.com):
86dfrom.com A record→ CloudFront distribution alias86from.com CNAME→ CloudFront redirect distribution (or A record with alias if set as apex)- ACM certificate validation CNAMEs for both domains
Both domains now route through CloudFront, with SSL/TLS provided by AWS Certificate Manager (auto-renewal, no certificate management burden).
API Integration & Variant Data Pipeline
Printful API Integration
The file scripts/get-printful-variants.js handles variant discovery:
// Pseudocode—actual implementation uses axios or node-fetch
const printfulKey = process.env.PRINTFUL_API_KEY;
const store = "86Store"; // Named store in Printful account
// Fetch products → Variants → Extract IDs for Bella+Canvas 3001 Black
// Filter to black variants (ignore heather/oxblood variants for MVP)
// Output: { small: "id", medium: "id", large: "id", xl: "id", xxl: "id" }
Why this approach? Hardcoding variant IDs is fragile. By fetching them programmatically, we can:
- Support multiple products/colors in the future without code changes
- Validate variant availability before rendering the form
- Audit which variants are active in Printful vs. our store
Variant IDs get stored in .env.local and referenced in the checkout API route.
Next.js API Routes for Checkout
We implemented the following routes:
pages/api/checkout.js— Accept cart data, create Stripe session, return checkout URLpages/api/webhook.js— Receive Stripe webhooks (payment_intent.succeeded, charge.failed, etc.) and trigger fulfillment orders in Printfulpages/api/variants.js— Return available sizes and pricing (optional, for dynamic UI)
Checkout flow:
- User selects size, quantity → POST to
/api/checkout - API validates variant ID exists, calculates price (Printful cost + markup)
- Creates Stripe Checkout session with
success_url→success.html,cancel_url→ index.html - Redirects browser to Stripe-hosted checkout
- On successful payment, Stripe posts webhook to
/api/webhook - Webhook validates signature, creates fulfillment order in Printful, logs to database/service
Security decisions:
- All API routes