```html

Building a Print-on-Demand T-Shirt Store on Next.js 14: Integrating Printful, Stripe, and AWS CloudFront

What Was Done

Over this session, we built out a complete production-ready print-on-demand (POD) t-shirt storefront at 86dfrom.com, integrating Printful's fulfillment API, Stripe for payments, and AWS infrastructure (S3 + CloudFront + Route53) for global CDN delivery. The site is a Next.js 14 application with API routes that communicate with Printful's variant endpoint to populate available products and sizes, then route successful orders to Stripe for payment processing and back to Printful for fulfillment.

The architecture spans two domains: the main storefront at 86dfrom.com and a legacy redirect at 86from.com (note the single "d"), both fronted by CloudFront distributions with ACM SSL certificates and Route53 DNS records.

Technical Details: The Stack

Frontend & API Layer

The Next.js 14 application lives in /Users/cb/Documents/repos/sites/86dfrom.com/ with the following structure:

  • site/index.html — Landing page with product showcase, variant selector, and Stripe checkout integration
  • site/success.html — Post-purchase confirmation page (served after webhook validation)
  • gas/Code.gs — Google Apps Script that logs orders to a Sheet (optional logging layer)
  • scripts/deploy.sh — Bash deployment script for S3 and CloudFront invalidation

The Next.js build compiles cleanly with zero errors. All 5 API routes (product variants, checkout session creation, webhook handling, order logging) compile without warnings. The frontend uses vanilla JavaScript for form handling — no heavy framework dependencies — keeping the initial load time minimal.

Product Variant Integration with Printful

Printful provides a REST API endpoint that returns available variants for a given store. We created scripts/get-printful-variants.js to fetch the exact variant IDs for the Bella+Canvas 3001 Black t-shirt (the primary product):

// Pattern: call Printful variant endpoint with store ID and product ID
// Returns JSON with variant_id, size, color, images
// Extract the 5 variant IDs (XS through XXL in Black)

These variant IDs are hardcoded into the API route at pages/api/variants.js, which serves them to the frontend as JSON. The frontend JavaScript then renders a dropdown selector and stores the chosen variant ID in the form submission payload.

Stripe Payment Flow

The checkout flow follows this pattern:

  1. User selects size/variant and clicks "Buy"
  2. Frontend POSTs to pages/api/checkout.js with variant ID and email
  3. API creates a Stripe Session with mode payment, sets success/cancel URLs, and stores order metadata
  4. API returns the Stripe session URL; frontend redirects to Stripe-hosted checkout
  5. After payment, Stripe POSTs to pages/api/webhook.js with checkout.session.completed event
  6. Webhook validates the signature (using STRIPE_WEBHOOK_SECRET), marks order as fulfilled, and triggers fulfillment to Printful

The webhook endpoint is critical — it validates the signature using the webhook secret (which Stripe provides after we register the endpoint in the dashboard). Without validation, any attacker could POST a fake completion event and trigger fulfillment.

Infrastructure: AWS & DNS Architecture

S3 Bucket Strategy

We created an S3 bucket named 86dfrom-com-static (or similar pattern, following the convention {domain}-static) to hold static assets (product images, CSS, JavaScript bundles). The bucket is not exposed directly — all traffic goes through CloudFront.

Bucket policy restricts public access and only allows CloudFront's Origin Access Identity (OAI) to read objects. This prevents direct S3 URLs from being abused and ensures all traffic is cached by CloudFront:

{
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
  },
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::86dfrom-com-static/*"
}

CloudFront Distribution for 86dfrom.com

The primary distribution:

  • Origin: 86dfrom-com-static.s3.amazonaws.com (S3 bucket)
  • Domain Names: 86dfrom.com, www.86dfrom.com
  • SSL/TLS: ACM certificate for both apex and www (requested, validated via Route53 CNAME, and provisioned)
  • Default Root Object: index.html
  • Viewer Protocol Policy: Redirect HTTP to HTTPS
  • Cache Behaviors: Static assets (CSS, JS, images) cached for 86400 seconds (1 day); HTML files cached for 60 seconds to allow for quick updates

CloudFront also applies gzip compression for text-based assets and serves via HTTP/2, reducing latency for global users.

CloudFront Function for Legacy Redirect (86from.com)

A second CloudFront distribution handles the typo domain 86from.com (missing "d") as a permanent redirect:

// CloudFront Function (viewer request trigger)
function handler(event) {
  var request = event.request;
  return {
    statusCode: 301,
    statusDescription: 'Moved Permanently',
    headers: {
      'location': { value: 'https://86dfrom.com' + request.uri }
    }
  };
}

This preserves any URL path (e.g., 86from.com/about86dfrom.com/about) and uses a 301 permanent redirect so search engines update their index.

Route53 DNS Configuration

Two A records (alias) point to the CloudFront distributions:

  • 86dfrom.com → CloudFront distribution (main site)
  • www.86dfrom.com → CloudFront distribution (same distribution handles both via alternate domain names)
  • 86from.com → Redirect CloudFront distribution

ACM certificate validation used Route53 CNAME records. Once validated, we deleted those validation records (Stripe and Printful don't need DNS records — they're API-only).

Key Decisions & Rationale

Why CloudFront for a Dynamic Store?

Even though the backend is Next.js (dynamic), CloudFront provides:

  • Global edge locations: Customers in EU, APAC, etc. download assets from nearby servers