```html

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 configuration
  • deploy.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:

  1. Build the site (if applicable)
  2. Sync /site directory to S3: aws s3 sync ./site s3://86dfrom-com-site
  3. Invalidate CloudFront cache: aws cloudfront create-invalidation --distribution-id --paths "/*"
  4. 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