Building a Printful-Integrated T-Shirt E-Commerce Site with Next.js 14, Stripe, and AWS CloudFront

This post documents the end-to-end build of 86dfrom.com, a Next.js 14 t-shirt store that integrates Printful's print-on-demand API, Stripe payments, and Google Apps Script webhook handlers. The architecture spans Vercel (app hosting), AWS S3 + CloudFront (static asset CDN), Route53 (DNS), and Google Sheets (order fulfillment backend).

Project Structure & Build Setup

The codebase lives at /Users/cb/Documents/repos/sites/86dfrom.com and is organized as follows:

  • site/ — Static landing page and success confirmation HTML
  • gas/ — Google Apps Script webhook handler (Code.gs) and manifest (appsscript.json)
  • scripts/ — Deployment and data-fetching utilities
  • .env.local — Environment variables for Printful, Stripe, and Google Sheets (git-ignored)

The Next.js 14 application compiles cleanly with no warnings. The build includes five routes: product listing, variant selection, checkout, webhook handler, and order confirmation. All routes are server-side rendered to support dynamic product data from Printful.

Printful API Integration & Variant Management

Printful powers the inventory and fulfillment. Rather than hardcoding variant IDs, we fetch them dynamically from Printful's API. The process:

  1. Authenticate to Printful using an API token scoped to the 86Store within the dangerouscentaur.com account
  2. Query the /products endpoint to list all products in the store
  3. For the Bella+Canvas 3001 unisex t-shirt, extract variant objects for the Black color (variants 4016–4020 covering XS–2XL)
  4. Store these variant IDs in .env.local as NEXT_PUBLIC_PRINTFUL_VARIANTS (comma-separated)

The scripts/get-printful-variants.js utility automates this fetch. It reads the Printful API key from environment, makes authenticated requests to list products and variants, filters for the correct color and size range, and outputs a formatted list ready for the .env.local template. This approach decouples product configuration from code, allowing inventory updates without redeployment.

Environment Configuration & Secrets Management

The .env.local file (never committed) contains three categories of secrets:

  • Printful: PRINTFUL_API_KEY (OAuth token for API calls), NEXT_PUBLIC_PRINTFUL_VARIANTS (public variant IDs for the storefront)
  • Stripe: STRIPE_SECRET_KEY (used server-side for payment processing), NEXT_PUBLIC_STRIPE_PUBLIC_KEY (used client-side for the Stripe.js embed), STRIPE_WEBHOOK_SECRET (added post-deployment after webhook endpoint is live)
  • Google Sheets: GOOGLE_SHEETS_WEBHOOK_URL (Apps Script endpoint for async order logging)

Stripe supports both test mode (sk_test_...) and live mode (sk_live_...) keys. Test mode allows sandbox payment testing without processing real cards; live mode charges customers. Both are retrieved from the Stripe Dashboard under Developers → API keys.

Infrastructure: AWS + Vercel + DNS

The deployment spans two infrastructure layers:

Vercel (App Server)

The Next.js 14 application deploys to Vercel via npx vercel@latest --prod. Environment variables are injected into the Vercel project dashboard, making them available at build and runtime. The production URL is assigned automatically by Vercel, and we configure a custom domain (86dfrom.com) via DNS CNAME.

AWS (Static CDN)

Static assets (product images, CSS, fonts) are served from an S3 bucket and cached via CloudFront. This architecture was chosen because:

  • Geographic distribution: CloudFront caches assets at 200+ edge locations worldwide, reducing latency for international customers
  • Cost efficiency: S3 + CloudFront is cheaper than serving large images from Vercel's edge network, especially for a high-traffic store
  • Asset versioning: CloudFront invalidations allow atomic cache busts when assets change (e.g., new product photos)

The S3 bucket is named 86dfrom.com (matching the domain for clarity). The bucket policy restricts public access to CloudFront's origin access identity, preventing direct S3 URL exposure. A CloudFront distribution with ID E... (to be filled after deployment) is configured to:

  • Origin: S3 bucket 86dfrom.com
  • Alternate domain names: cdn.86dfrom.com, assets.86dfrom.com
  • SSL certificate: ACM certificate for 86dfrom.com (auto-renewed by AWS)
  • Compression: enabled for CSS, JavaScript, JSON
  • Cache behavior: 24-hour TTL for images, 1-hour TTL for HTML

Route53 (AWS's DNS service) manages the zone for 86dfrom.com and handles:

  • A record (86dfrom.com) → Vercel IP (assigned via CNAME to Vercel's domain)
  • CNAME for cdn.86dfrom.com → CloudFront distribution domain
  • ACM certificate DNS validation records (auto-added for HTTPS)

Stripe Webhook Handler

Stripe webhooks notify the backend when payment events occur (e.g., payment_intent.succeeded). The webhook endpoint is at /api/webhook and verifies the Stripe signature using STRIPE_WEBHOOK_SECRET. Upon successful verification, the webhook:

  1. Extracts order metadata (customer email, product variant, size)
  2. Sends a POST request to the Google Apps Script endpoint
  3. The Apps Script logs the order into a Google Sheet for fulfillment staff
  4. Returns a 200 status to Stripe, confirming receipt

The webhook secret is obtained after the Vercel deployment is live. In the Stripe Dashboard, under Developers → Webhooks, we add an endpoint for https://86dfrom.com/api/webhook and copy the generated secret into .env.local and the Vercel environment dashboard.

Google Apps Script Backend

The gas/Code.gs file defines a doPost function that receives webhook payloads and appends rows to a Google Sheet. This decouples order storage from