Building a Print-on-Demand T-Shirt Site with Next.js 14, Printful, and Stripe: Infrastructure & Deployment Strategy
Overview
We built a full-stack e-commerce platform for 86dfrom.com — a print-on-demand t-shirt storefront powered by Next.js 14, integrated with Printful's API for inventory management, and Stripe for payment processing. This post details the architecture decisions, deployment pipeline, and infrastructure setup that enables seamless order fulfillment.
Project Structure & Build Setup
The project lives at /Users/cb/Documents/repos/sites/86dfrom.com with a clean Next.js 14 foundation:
/site— Frontend HTML (static assets, landing page, success confirmation)/gas— Google Apps Script integration (webhook processing, data logging)/scripts— Deployment automation and data fetching utilities.env.local— Environment configuration (Stripe keys, Printful token, webhook secrets)appsscript.json— Google Apps Script manifest with OAuth scopes
The Next.js build compiles all 5 API routes cleanly with zero errors:
npm run build
# Output: compiled successfully ✓
This validates that route handlers, middleware, and dependencies are correctly wired before deployment.
Printful Integration: API Architecture
We use Printful as the single source of truth for product inventory and variant management. The integration flow:
- Variant Discovery —
scripts/get-printful-variants.jsfetches the Bella+Canvas 3001 Black t-shirt (product ID from store) and extracts all size/color variants, writing their IDs to.env.local - Inventory Lookup — API route
/api/variantsreturns available sizes and pricing by reading stored variant IDs - Order Creation —
/api/create-orderaccepts a variant ID + size selection, calculates Stripe charge, then submits fulfillment order to Printful
The key decision: store variant IDs in env rather than querying Printful on every request. This reduces API calls, improves latency, and lets us version control product catalogs. Variant IDs are immutable within a store, so a one-time fetch is sufficient.
Example variant ID storage in .env.local:
NEXT_PUBLIC_PRINTFUL_VARIANT_4016=4016
NEXT_PUBLIC_PRINTFUL_VARIANT_4017=4017
NEXT_PUBLIC_PRINTFUL_VARIANT_4018=4018
NEXT_PUBLIC_PRINTFUL_VARIANT_4019=4019
NEXT_PUBLIC_PRINTFUL_VARIANT_4020=4020
PRINTFUL_API_KEY=[redacted]
Payment Processing: Stripe Webhook Architecture
Stripe handles payment collection and fraud detection. Our webhook flow:
- Customer submits order via
/api/create-order, which callsstripe.paymentIntents.create() - Stripe returns a payment intent and client secret to the frontend
- Frontend uses Stripe.js to confirm payment (3D Secure, etc.)
- On success, webhook at
/api/webhookreceivespayment_intent.succeededevent - Webhook verifies signature using stored
STRIPE_WEBHOOK_SECRET - On verification, we create Printful fulfillment order and log to Google Sheets
This architecture decouples payment confirmation from order fulfillment, allowing retries and audit trails. The webhook secret is unique per environment (test vs. live), so we store separate keys in Vercel.
Infrastructure: DNS, CDN, and Static Hosting
S3 + CloudFront Setup
We deployed static assets to S3 and served via CloudFront for global caching:
- S3 Bucket:
86dfrom-site-production(region: us-east-1) - CloudFront Distribution ID: (retrieved via AWS CLI; cached in project docs)
- ACM Certificate: Wildcard cert for
*.86dfrom.com(issued, DNS validated in Route53) - Origin: S3 bucket with origin access identity (OAI) restricting public reads
CloudFront configuration enforces HTTPS, compresses text assets, and invalidates cache on deploy via:
aws cloudfront create-invalidation --distribution-id [ID] --paths "/*"
Route53 DNS Records
Domain registrar holds authoritative NS records pointing to Route53 hosted zone:
- A record:
86dfrom.com→ CloudFront distribution alias - CNAME:
www.86dfrom.com→ CloudFront distribution - CNAME:
86from.com→ separate CloudFront redirect distribution (typo-squatting protection)
ACM certificate validation records were added as CNAMEs and validated automatically by AWS.
Redirect Distribution (86from.com)
We created a secondary CloudFront distribution with a CloudFront Function that redirects 86from.com/* to 86dfrom.com/*. This is deployed via scripts/deploy.sh, which:
#!/bin/bash
# Create/update CloudFront redirect function
aws cloudfront create-function \
--name 86from-redirect \
--auto-publish \
--function-code fileb://redirect-function.js
# Deploy to redirect distribution
aws cloudfront create-distribution-with-tags \
--distribution-config-with-tags [config]
This pattern catches traffic to the typo domain and funnels users to the correct site without requiring a separate S3 bucket.
Vercel Deployment Pipeline
The Next.js app (dynamic routes: /api/*) is deployed to Vercel production via:
npx vercel@latest --prod
Environment variables are set post-deploy:
STRIPE_SECRET_KEY— Live or test secret keySTRIPE_WEBHOOK_SECRET— Endpoint secret from webhook configurationPRINTFUL_API_KEY— Store token with all scopesNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY— Public key for frontend- Variant ID env vars (prefixed
NEXT_PUBLIC_PRINTFUL_VARIANT_*)
Vercel's deployment creates a production URL (e.g., 86dfrom.vercel.app initially), which becomes the API backend. Route53 CNAME records point api.86dfrom.com to this Vercel domain,