Building a Printful + Stripe T-Shirt Store with Next.js 14: Infrastructure & Deployment Pipeline
This post documents the technical implementation of 86dfrom.com, a Next.js 14–based t-shirt storefront integrated with Printful's print-on-demand API and Stripe for payments. We'll cover the architecture decisions, infrastructure setup, and deployment pipeline that powers the site.
Project Structure & Technology Stack
The project lives in /Users/cb/Documents/repos/sites/86dfrom.com and is organized as follows:
/site— Static HTML landing page and success confirmation page/gas— Google Apps Script webhook handlers for form submissions/scripts— Node.js automation scripts for API integration and deployment.env.local— Runtime environment variables (Printful API key, Stripe keys)appsscript.json— Google Apps Script manifest for webhook configurationdeploy.sh— Shell script orchestrating S3 and CloudFront updates
The tech stack uses Next.js 14 for the API layer (deployed to Vercel), Google Apps Script for email/form submission webhooks, and Printful API v2 for print fulfillment. All static assets are served from S3 + CloudFront with Route53 DNS management.
Printful API Integration
The Printful integration starts with product variant discovery. The Printful API requires a store token (generated at printful.com → Dashboard → Settings → API). Rather than hardcoding variant IDs, we built a script to fetch them dynamically.
The script /scripts/get-printful-variants.js queries the Printful Products endpoint to discover available variants for the Bella+Canvas 3001 blank (Black colorway). This approach decouples the codebase from Printful's catalog; if variants change or new colors are added, a single script run updates the configuration.
// Pseudocode for variant discovery
const response = await fetch('https://api.printful.com/products', {
headers: { 'Authorization': `Bearer ${PRINTFUL_API_KEY}` }
});
const products = await response.json();
const variants = products
.filter(p => p.externalId === 'BELLA_CANVAS_3001')
.flatMap(p => p.variants)
.filter(v => v.color === 'Black');
Why this approach? The Bella+Canvas 3001 comes in multiple colorways and fits. By parameterizing variant selection, we can easily test pricing or inventory across different configurations without touching application code.
Infrastructure: S3, CloudFront, and Route53
The static site (landing page, success confirmation, and assets) is hosted on S3 with CloudFront serving as the content delivery network and SSL terminator. This pattern separates concerns: Vercel handles dynamic API routes, while S3 + CloudFront serves static content with geographic caching.
S3 Bucket Configuration:
- Bucket name:
86dfrom-com-site - Region:
us-east-1(CloudFront origin requirement) - Bucket policy: Restrict public access, allow CloudFront Origin Access Identity (OAI) only
- Static website hosting: Disabled (CloudFront is the public endpoint)
CloudFront Distribution:
The primary distribution (86dfrom.com) points to the S3 bucket via an OAI, ensuring no direct S3 access. The distribution includes:
- Origin: S3 bucket endpoint with OAI authentication
- Behaviors: Cache index.html with short TTL (60s), static assets with long TTL (31536000s)
- Viewer Protocol Policy: Redirect HTTP → HTTPS
- Default Root Object:
index.html - Lambda@Edge Function: Custom request handler to rewrite URLs (e.g.,
/→/index.html)
Lambda@Edge redirect logic was necessary because CloudFront doesn't natively rewrite paths. The function checks if a request is for a directory or non-existent object and appends index.html:
exports.handler = (event, context, callback) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
if (uri.endsWith('/') || !uri.includes('.')) {
request.uri += 'index.html';
}
callback(null, request);
};
Route53 Configuration:
DNS records were added in the 86dfrom.com hosted zone:
- A record:
86dfrom.com→ CloudFront distribution (alias) - AAAA record:
86dfrom.com→ CloudFront distribution (IPv6 alias) - ACM validation: CNAME records for SSL certificate validation
A second CloudFront distribution handles a redirect: 86from.com (typo variant) redirects to 86dfrom.com via another Lambda@Edge function that returns a 301 response.
Deployment Pipeline
The deploy script (/scripts/deploy.sh) orchestrates multiple steps:
- Build the site (if applicable)
- Sync
/sitedirectory to S3:aws s3 sync ./site s3://86dfrom-com-site - Invalidate CloudFront cache:
aws cloudfront create-invalidation --distribution-id--paths "/*" - Wait for invalidation to complete
Why CloudFront invalidation? S3 objects are immutable; they don't get updated in place. Invalidation clears the edge cache, forcing CloudFront to fetch fresh objects on the next request. A full-path invalidation (/*) ensures all content is refreshed.
Vercel Deployment & Environment Variables
The Vercel deployment hosts the Next.js 14 application with these key routes:
/api/variants— Returns available product variants (populated from Printful)/api/orders— Accepts order creation requests and forwards to Printful/api/webhook— Receives Stripe webhook events for payment confirmation
Environment variables are stored in Vercel's dashboard under Project Settings → Environment Variables. These include:
PRINTFUL_API_KEY— Obtained from Printful store settings