Building a Printful-Integrated T-Shirt Commerce Site on Next.js 14: Infrastructure, Deployment, and API Integration
Over the past development session, we completed a full-stack e-commerce site for 86dfrom.com—a print-on-demand t-shirt store powered by Next.js 14, Printful, Stripe, and AWS CloudFront. This post covers the architecture decisions, infrastructure setup, and deployment pipeline we implemented.
Project Structure and Technology Stack
The site is built as a Next.js 14 application with the following directory layout:
/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│ ├── index.html (Static landing page)
│ └── success.html (Post-purchase confirmation)
├── gas/
│ ├── Code.gs (Google Apps Script backend)
│ ├── appsscript.json (GAS manifest)
│ └── .clasp.json (Clasp deployment config)
├── scripts/
│ ├── deploy.sh (S3 + CloudFront deployment)
│ └── get-printful-variants.js (Variant ID population)
└── .env.local (Runtime configuration)
The Next.js app includes five API routes that handle product variants, Stripe payment processing, and Printful order fulfillment. A clean build via npm run build confirms zero compilation errors across all routes.
Infrastructure Architecture: AWS + Stripe + Printful
Why this stack? Printful abstracts print-on-demand fulfillment, Stripe handles PCI compliance for payments, and AWS provides global CDN distribution. This eliminates inventory management and PCI scope while keeping operational costs low.
S3 and CloudFront Deployment
We created dedicated S3 buckets for both 86dfrom.com and a redirect-only 86from.com domain:
- Primary bucket:
86dfrom.com— hosts the static site files - Redirect bucket:
86from.com— 301 redirects to primary domain
Each bucket is paired with a CloudFront distribution:
- Primary distribution: Serves site/index.html and success.html with cache control headers, origin request behavior pointing to the S3 bucket
- Redirect distribution: Uses a CloudFront Function to intercept all requests and emit 301 redirects to the primary domain
The redirect function (published via aws cloudfront create-function) looks like this in pseudocode:
function handler(request) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
'location': { value: 'https://86dfrom.com' + request.uri }
}
}
}
This approach avoids maintaining dual S3 content and keeps redirect logic centralized in the CDN layer.
DNS and Certificate Management
Route53 hosted zone for 86dfrom.com contains:
- A record (86dfrom.com): Alias pointing to primary CloudFront distribution
- CNAME record (www.86dfrom.com): Alias pointing to primary CloudFront distribution
- A record (86from.com): Alias pointing to redirect CloudFront distribution
ACM certificates for both domains were requested and validated via DNS CNAME records added to Route53. This automation eliminates manual domain verification and keeps DNS centralized in one control plane.
Deployment Pipeline
Static Site Deployment: The scripts/deploy.sh script handles S3 sync and CloudFront cache invalidation:
#!/bin/bash
aws s3 sync ./site s3://86dfrom.com --delete --region us-east-1
aws cloudfront create-invalidation --distribution-id DISTRIBUTION_ID --paths "/*"
The --delete flag ensures stale files are removed on each deployment. CloudFront invalidation clears the edge cache immediately, avoiding stale content.
Next.js Vercel Deployment: The Next.js application (handling API routes) deploys to Vercel via npx vercel@latest --prod. Environment variables are injected post-deployment through Vercel's dashboard or CLI.
API Route Design and Environment Configuration
The Next.js app exposes five key endpoints:
- /api/variants: Queries Printful API to return available t-shirt variants and pricing
- /api/checkout: Creates a Stripe Checkout session, passing line items mapped to Printful variant IDs
- /api/webhook: Receives Stripe webhook events (payment success, failure) and triggers Printful order creation
- Two product-specific routes: Handle variant selection and order confirmation state
Configuration is driven by .env.local, which contains:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
PRINTFUL_API_KEY=...
NEXT_PUBLIC_STORE_ID=86Store
Note the NEXT_PUBLIC_ prefix: only the publishable key is exposed to the browser. The secret key and webhook secret remain backend-only, protecting payment credentials.
Printful Integration
Variant IDs are populated by running scripts/get-printful-variants.js, which calls the Printful API endpoint to fetch all available variants for the configured store. This script must run once per new product to populate the environment with variant IDs:
node scripts/get-printful-variants.js
The script outputs variant IDs for the Bella+Canvas 3001 Black t-shirt (the default SKU). These IDs are hardcoded into the checkout logic to ensure line items are mapped correctly to Printful's product catalog.
Key Decisions and Trade-offs
- Static S3 + CloudFront vs. Next.js SSR: We split concerns: static landing pages live on S3 (cacheable, no backend needed), while dynamic checkout and API routes run on Vercel (serverless, scales to zero). This reduces costs and complexity.
- Redirect domain via CloudFront Function: Rather than running a redirect server, we use a lightweight CloudFront Function (executes at edge, <1ms latency). This avoids maintaining another compute resource.
- ACM DNS validation: We use Route53 DNS validation instead of email verification, automating cert renewal and avoiding expired certificate incidents.
- Google Apps Script backend (gas/): The GAS layer provides an alternative webhook handler and integrates with Google Sheets for order tracking.
.clasp.jsonconfig stores the deployment mapping.
What's Next
Remaining tasks before launch: