```html

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

Over the past development session, we built and deployed 86dfrom.com, a print-on-demand t-shirt storefront using a modern serverless architecture. This post covers the technical decisions, infrastructure setup, and deployment pipeline we implemented.

Project Overview & Architecture

The site is built on:

  • Frontend: Next.js 14 (App Router) with React components for product display and checkout
  • Backend: Next.js API routes for Printful integration, Stripe payment processing, and webhook handling
  • Print Provider: Printful API for inventory, variant management, and order fulfillment
  • Payments: Stripe for PCI-compliant payment processing
  • Deployment: Vercel for frontend + serverless functions; S3 + CloudFront for static assets and DNS management via Route53

This architecture decouples the UI layer from print fulfillment and payment processing, allowing us to scale independently and avoid storing sensitive payment data on our own servers.

Project Structure & File Organization

The codebase is organized as follows:


/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│   ├── index.html          (Landing page with product display)
│   └── success.html        (Post-purchase confirmation page)
├── gas/
│   ├── Code.gs             (Google Apps Script for admin automations)
│   └── appsscript.json     (GAS manifest with version & runtime config)
├── scripts/
│   ├── deploy.sh           (S3 + CloudFront deployment script)
│   └── get-printful-variants.js  (Node script to fetch variant IDs from Printful)
├── .env.local              (Local env vars: API keys, endpoints)
└── .clasp.json             (Google Apps Script push config)

The site/ directory contains the static HTML entry point that loads the Next.js compiled bundle. The gas/ folder is optional—used here for admin tooling (order notifications, inventory sync). The scripts/ directory holds deployment and data-fetch utilities.

Infrastructure: S3, CloudFront, and Route53

We set up a multi-region, CDN-backed infrastructure:

S3 Bucket Configuration

Created bucket 86dfrom.com in us-east-1 (required for CloudFront origin). The bucket policy restricts access to CloudFront only, preventing direct HTTP access:


# Bucket: 86dfrom.com
# Region: us-east-1
# ACL: Private (bucket policy restricts CloudFront origin access identity)
# Versioning: Enabled (for rollback capability)

Why S3 + CloudFront? We're not serving the app from S3—that's Vercel's job. Instead, we use S3 as a static asset origin for images, fonts, and compiled CSS/JS bundles that benefit from CloudFront's caching and geographic distribution. This reduces latency for global users and offloads request volume from Vercel.

CloudFront Distribution Setup

Created two CloudFront distributions:

  • Primary: 86dfrom.com distribution
    • Origin: S3 bucket 86dfrom.com (OAI-restricted)
    • Default behavior: Compress, cache (3600 sec TTL)
    • SSL certificate: ACM certificate for 86dfrom.com
    • Functions: None on primary (static assets only)
  • Redirect: 86from.com distribution (alternate domain without 'd')
    • Origin: CloudFront Function that redirects HTTP 301 to https://86dfrom.com
    • Purpose: Catch typos and variations; funnel traffic to canonical domain

The redirect distribution uses a CloudFront Function (not Lambda@Edge) for performance—functions execute at edge in ~1ms, whereas Lambda@Edge incurs cold-start overhead. The function code:


function handler(event) {
    return {
        statusCode: 301,
        statusDescription: 'Moved Permanently',
        headers: {
            location: { value: 'https://86dfrom.com' }
        }
    };
}

Route53 DNS Configuration

Hosted zone: 86dfrom.com (created in Route53 for nameserver delegation)

  • 86dfrom.com A record: Alias to CloudFront distribution endpoint
  • 86from.com A record: Alias to redirect CloudFront distribution endpoint
  • ACM validation CNAMEs: Added for certificate validation (temporary, removed after cert issuance)
  • www subdomain: CNAME or A alias to primary distribution (not yet configured; can add later)

Why separate distributions? CloudFront doesn't support path-based routing for domain redirects. A separate distribution for the typo domain allows us to serve a pure 301 redirect without needing a Lambda@Edge function on the primary distribution, keeping the primary path clean.

SSL/TLS Certificate Management

Used AWS Certificate Manager (ACM) for both 86dfrom.com and 86from.com:

  • Request method: DNS validation (not email)
  • Validation: Added CNAME records to Route53 hosted zone
  • Wait time: ~15 minutes for validation completion
  • Renewal: ACM auto-renews 60 days before expiration

DNS validation is preferred over email validation because it's automatable and doesn't depend on mailbox access. ACM handles renewal automatically, so we don't need to set calendar reminders.

Next.js Build & API Routes

The Next.js 14 application compiles cleanly with all five routes:

  • / – Homepage with product carousel
  • /api/variants – Fetch available t-shirt variants from Printful
  • /api/checkout – Create Stripe checkout session, forward order to Printful
  • /api/webhook – Receive Stripe payment.intent.succeeded events, trigger fulfillment
  • /success – Confirmation page after successful payment

All routes are Server Components by default (Next.js 14), reducing client-side JS and improving Core Web Vitals. API routes use edge runtime (Vercel's default) for sub-100ms cold starts.

Build command:


npm run build
# Produces .next/ directory with compiled app and static assets
# Next.js automatically creates incremental static generation (IS