Building a Direct-to-Consumer T-Shirt Site on Vercel + AWS: 86dfrom.com Infrastructure & Deployment Strategy

What We Built

We deployed a full-stack e-commerce site for print-on-demand t-shirts using Next.js 14, Printful API integration, Stripe payments, and AWS CloudFront + S3 for static asset delivery. The architecture spans multiple deployment targets: Vercel for the Next.js application, S3 + CloudFront for the primary domain, and a redirect distribution for domain variants.

Project Structure

The codebase is organized as follows:

  • /site/ — Static HTML landing pages (index.html, success.html) deployed to S3
  • /gas/ — Google Apps Script backend for auxiliary workflows (Code.gs, appsscript.json)
  • /scripts/ — Deployment and utility scripts (deploy.sh)
  • .env.local — Local environment variables for Printful, Stripe keys

The main Next.js application contains five API routes that compile cleanly: product variants, checkout handling, webhook processing, and Stripe integration endpoints.

Infrastructure Architecture

Primary Domain: 86dfrom.com

The primary domain uses a three-layer AWS setup:

  • S3 Bucket: 86dfrom.com — Stores static site files (index.html, success.html, CSS, client-side JS)
  • CloudFront Distribution: Created via AWS Console with origin pointing to the S3 bucket. The distribution ID and domain name are used in Route53 DNS records.
  • ACM Certificate: Requested for 86dfrom.com with DNS validation via CNAME records added to Route53. Certificate validation takes ~5–10 minutes after DNS propagation.
  • Route53 Hosted Zone: Existing zone for 86dfrom.com receives an A record (alias) pointing to the CloudFront distribution domain.

The S3 bucket policy is configured to allow CloudFront distribution access via an Origin Access Identity (OAI), preventing direct public access to objects. This pattern ensures:

  • All requests flow through CloudFront (enabling caching, WAF rules, and compression)
  • S3 bucket remains private; no public object ACLs required
  • Cache invalidation via CloudFront API (used in deploy scripts) updates content instantly

Variant Domain: 86from.com (No "d")

A redirect distribution was created to handle the common misspelling. This uses:

  • CloudFront Function: A lightweight viewer-request function that redirects HTTP(S) requests from 86from.com to 86dfrom.com with a 301 permanent redirect.
  • Redirect CloudFront Distribution: Minimal distribution (no S3 origin) with the CloudFront Function attached. Origin is a dummy HTTPS domain to satisfy CloudFront's requirement for an origin.
  • Route53 A Record: Points 86from.com to the redirect distribution's domain name.

This pattern is lightweight and cost-effective: CloudFront Functions execute at edge locations with minimal latency and no compute charges (billed per 1M invocations).

Deployment Pipeline

Static Site Deployment (S3 + CloudFront)

The deploy script at scripts/deploy.sh follows this sequence:

# Example deployment steps (actual credentials redacted)
aws s3 sync ./site s3://86dfrom.com --delete
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"

Key decisions:

  • --delete flag: Ensures removed files are purged from S3, preventing stale assets from being served
  • Invalidation path "/*": Clears all CloudFront cache edges, guaranteeing fresh content within ~30 seconds globally
  • Sequential execution: S3 sync completes before invalidation is triggered, avoiding race conditions

Vercel Deployment (Next.js Application)

The Next.js application is deployed separately to Vercel using:

npx vercel@latest --prod

Environment variables (Printful API key, Stripe secret/publishable keys, webhook secret) are stored in Vercel's project settings under Settings → Environment Variables, with separate configurations for preview and production deployments.

The application build is verified clean with npm run build before any deployment attempt. All five API routes compile without errors in Next.js 14's strict mode.

Printful Integration

Product variant IDs are populated by running a utility script:

node scripts/get-printful-variants.js

This script queries the Printful API for the Bella+Canvas 3001 t-shirt in Black colorway (variant IDs 4016–4020 for sizes XS–2XL). The script:

  • Authenticates using the Printful API key from .env.local
  • Filters by product (3001) and color (Black) to isolate the correct SKU
  • Returns an array of variant objects with pricing, inventory, and printable areas
  • Output is parsed and stored as environment variables for API routes

Why this approach? Hardcoding variant IDs is brittle if Printful's catalog changes. Querying the API at startup ensures the site always reflects current inventory and pricing.

Stripe Payment Integration

Stripe keys are required at two stages:

  • Publishable Key: Embedded in client-side checkout forms, used by Stripe.js to create payment tokens
  • Secret Key: Stored server-side in Vercel env vars, used in API routes to create charges and handle webhooks

The webhook endpoint at /api/webhook processes Stripe events (payment_intent.succeeded, charge.failed) to update order status. The webhook signing secret (obtained after deploying to Vercel and configuring the endpoint URL in Stripe Dashboard) is used to verify webhook authenticity via HMAC-SHA256 signature validation.

DNS Configuration

Route53 records for 86dfrom.com include:

  • A Record (Alias): 86dfrom.com → CloudFront distribution domain (e.g., d1234.cloudfront.net)
  • AAAA Record (Alias): IPv6 variant pointing to the same CloudFront distribution
  • CNAME Records: For ACM certificate DNS validation (auto-managed by AWS once validation is requested)

Why alias records instead of CNAME? AWS Route53 alias records are zone apex-compatible, allowing DNS at the root domain (86dfrom.com) without the subdomain requirement that CNAME records impose.

Key Decisions & Rationale