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.comdistribution- 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)
- Origin: S3 bucket
- Redirect:
86from.comdistribution (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
- Origin: CloudFront Function that redirects HTTP 301 to
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