```html

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

Overview

This post documents the technical architecture and deployment workflow for 86dfrom.com, a print-on-demand (POD) t-shirt e-commerce site built on Next.js 14, integrated with Printful's fulfillment API and Stripe for payments. We'll cover the full stack: local development setup, API integration patterns, S3+CloudFront static hosting for the marketing site, and Vercel deployment for the Next.js app.

What Was Done

Project Structure & Local Setup

The project lives at /Users/cb/Desktop/86dfrom with a monorepo-like structure:

  • site/ — Static marketing HTML (index.html, success.html)
  • gas/ — Google Apps Script deployment files (Code.gs, appsscript.json)
  • scripts/ — Utility scripts for deployment and variant population
  • Next.js root — app/ directory with API routes and pages

The Next.js build passed clean with all 5 routes compiling successfully. This confirms the code is ready for production before any credential injection.

Printful API Integration

Printful API key was provisioned for the "Hello Dangerous" account with an 86Store sub-account. The integration requires:

  • Variant ID population: A script at scripts/get-printful-variants.js queries the Printful API endpoint https://api.printful.com/store/{store_id}/products to retrieve Bella+Canvas 3001 black t-shirt variant IDs (SKUs 4016–4020)
  • Environment injection: Variant IDs stored in .env.local with keys like NEXT_PUBLIC_VARIANT_ID_XS, NEXT_PUBLIC_VARIANT_ID_S, etc.
  • API routes: app/api/submit-order.js and app/api/webhook.js handle order submission to Printful and webhook validation respectively

The Printful token has full scope across all stores, valid through 2028, and the API call pattern follows their REST specification with bearer token auth in the Authorization header.

Stripe Payment Integration

Stripe integration uses both publishable and secret keys in a standard client-server split:

  • Client-side: Publishable key embedded in .env.local as NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
  • Server-side: Secret key in .env.local as STRIPE_SECRET_KEY, used only in API routes (app/api/webhook.js)
  • Webhook: Endpoint at /api/webhook configured in Stripe Dashboard to validate order payments and trigger fulfillment

The webhook secret (whsec_...) will be generated after Vercel deployment and registered in both Vercel env vars and Stripe's webhook settings.

Infrastructure Architecture

Static Site Hosting (Marketing Pages)

The marketing HTML files (site/index.html, site/success.html) are deployed to AWS infrastructure:

  • S3 bucket: 86dfrom.com (regional bucket for lowest-cost static hosting)
  • CloudFront distribution: Primary distribution fronts the S3 bucket, caches HTML/CSS/JS globally, and invalidates on deploy
  • ACM certificate: Wildcard cert for *.86dfrom.com and 86dfrom.com with DNS validation via Route53
  • Route53 hosted zone: Contains A and AAAA records pointing to the CloudFront distribution alias

A second CloudFront distribution handles the legacy 86from.com domain as a redirect-only distribution using CloudFront Functions, mapping traffic to the canonical 86dfrom.com without a 301 redirect (preserving the address bar appearance).

Next.js App Deployment

The Next.js application is deployed to Vercel, which:

  • Automatically builds on push to the connected Git repository
  • Serves the app at https://86dfrom.com via Vercel's edge network
  • Stores all environment variables (Stripe keys, Printful API key, variant IDs) in the Vercel project settings
  • Provides serverless function execution for /api/submit-order and /api/webhook routes

Deployment Automation

The scripts/deploy.sh script automates the full deployment pipeline:

#!/bin/bash
# Example structure (no actual secrets shown)
export AWS_ACCESS_KEY_ID=[redacted]
export AWS_SECRET_ACCESS_KEY=[redacted]

# Build Next.js
npm run build

# Sync site/ directory to S3
aws s3 sync site/ s3://86dfrom.com --delete

# Invalidate CloudFront cache
aws cloudfront create-invalidation \
  --distribution-id [CLOUDFRONT_DIST_ID] \
  --paths "/*"

# Deploy to Vercel (requires VERCEL_TOKEN in environment)
npx vercel@latest --prod

Key Technical Decisions

Why S3 + CloudFront for Static Content?

While Vercel could serve static files, separating static marketing content (S3) from dynamic app logic (Vercel) provides cost efficiency and independent cache invalidation. Marketing pages rarely change; Vercel deployments don't need to invalidate them.

Why Environment Variables Over Config Files?

Variant IDs, API keys, and secret keys are injected via .env.local (local dev) and Vercel's UI (production). This prevents hardcoding sensitive data in source control and enables environment-specific configurations (test vs. live Stripe keys).

Why CloudFront Functions for Domain Redirect?

A traditional 301 redirect requires a backend. CloudFront Functions execute at edge locations with sub-millisecond latency and zero compute cost, making them ideal for simple URL rewrites. The function intercepts requests to 86from.com and internally rewrites them to 86dfrom.com.

Why Bearer Token Auth for Printful?

Printful's API uses OAuth 2.0-style bearer tokens. The token is stored server-side in Vercel env vars and injected into request headers via Authorization: Bearer ${PRINTFUL_API_KEY}, preventing exposure to the client.

Environment Variables Checklist

Before deploying, these env vars must be populated in .env.local (local) and Vercel project settings (production):