```html

Building a Print-on-Demand T-Shirt Store with Next.js 14, Printful, and Stripe: Infrastructure & Deployment Strategy

Over the past development session, we built out the complete infrastructure and deployment pipeline for 86dfrom.com, a print-on-demand t-shirt store powered by Next.js 14, Printful's API, and Stripe payments. This post details the technical decisions, infrastructure setup, and deployment patterns we implemented.

Project Architecture Overview

The project structure follows a clean separation of concerns:

  • Frontend: Next.js 14 with 5 API routes (product catalog, variant details, checkout, webhook receiver, order status)
  • Backend Services: Printful API for inventory/fulfillment, Stripe for payment processing
  • Infrastructure: Vercel for hosting, Route53 for DNS, CloudFront for static asset delivery, S3 for origin storage
  • Automation: Bash deployment scripts, Google Apps Script for order notifications

The build pipeline was validated clean—all 5 routes compile without errors, confirming the Next.js 14 configuration is production-ready.

Printful API Integration & Variant Management

Rather than hardcoding product variant IDs, we created a programmatic approach to fetch and cache them. The Printful API requires:

  • Store ID: Retrieved from the Printful dashboard under Settings → Stores
  • API Token: Generated at Settings → API with full scope across all stores

The workflow involves:

  1. Running scripts/get-printful-variants.js to query the Printful API and extract variant IDs for a specific product (in this case, Bella+Canvas 3001 Black t-shirt)
  2. Storing these IDs in .env.local as comma-separated values for reference in API routes
  3. Using those IDs when constructing POST requests to Printful's order creation endpoint

This approach decouples product definitions from code deployment—if Printful changes variant IDs, you update the script output and redeploy environment variables without touching source code.

Environment Configuration & Secrets Management

We follow a strict separation of development and production secrets:

  • .env.local (development): Contains test API keys, Printful token, test Stripe secret
  • Vercel Environment Variables: Production keys, synced via the Vercel CLI during deploy
  • Google Cloud Secrets (optional): For sensitive values shared across multiple services

The .env.local file structure includes:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
PRINTFUL_API_TOKEN=...
PRINTFUL_STORE_ID=...
PRINTFUL_PRODUCT_VARIANT_IDS=4016,4017,4018,4019,4020
WEBHOOK_SECRET=whsec_...

Note: NEXT_PUBLIC_ prefix is used only for the Stripe publishable key, which is safe to expose client-side. All secret keys remain server-side only.

DNS & CloudFront Distribution Architecture

The domain 86dfrom.com required multi-step AWS setup:

Route53 Hosted Zone

Created a new hosted zone for 86dfrom.com in Route53. This gave us authoritative nameservers to configure at the domain registrar.

ACM Certificate

Requested an ACM certificate for both 86dfrom.com and www.86dfrom.com (wildcard not used to avoid including 86from.com). The certificate required DNS validation:

  • ACM provided CNAME records (typically _xxxxx.86dfrom.com → _yyyyy.acm-validations.aws)
  • We added these CNAME records directly in Route53 via CLI
  • AWS validated ownership within minutes

CloudFront Distribution (Primary)

Created the main distribution (86dfrom.com) with:

  • Origin: Vercel deployment (auto-detected as HTTP origin, CloudFront handles HTTPS to origin)
  • Alternate domain names: 86dfrom.com, www.86dfrom.com
  • SSL certificate: The ACM certificate from above
  • Cache behaviors: Short TTLs (60s) for dynamic content (/api/*), longer TTLs (3600s) for static assets
  • Origin request policy: Forward all headers to Vercel (required for Next.js cookie-based sessions)

Route53 A records alias to the CloudFront distribution:

86dfrom.com → CloudFront dist ID (A record, alias)
www.86dfrom.com → CloudFront dist ID (A record, alias)

CloudFront Redirect Distribution (Secondary)

A separate distribution was created to handle 86from.com (typo domain) with a CloudFront function that redirects all traffic to 86dfrom.com:

  • Function type: CloudFront Functions (viewer request)
  • Logic: Returns HTTP 301 redirect with Location: https://86dfrom.com
  • Rationale: Cheap redirect layer (Functions are free, unlike Lambda@Edge); no backend involved

This pattern avoids paying for traffic through a backend and prevents search engine confusion about canonical domain.

Deployment Pipeline

The deployment strategy uses a two-stage approach:

Stage 1: Vercel Production Deploy

Command:

npx vercel@latest --prod

This:

  • Builds the Next.js 14 project in Vercel's environment
  • Runs the build script from package.json
  • Detects all 5 API routes and deploys them as serverless functions
  • Assigns a temporary Vercel URL (86dfrom-xxx.vercel.app) as the origin for CloudFront

Environment variables are set via Vercel CLI before deploy or added manually in the Vercel dashboard under Settings → Environment Variables.

Stage 2: CloudFront Cache Invalidation

After deploy, any edge-cached routes must be invalidated:

aws cloudfront create-invalidation \
  --distribution-id E1234ABCD5678 \
  --paths "/*"

This clears CloudFront's cache, forcing edge nodes to fetch fresh content from Vercel on next request.