```html

Building a Next.js 14 Print-on-Demand Store: Multi-Domain Deployment, Printful Integration, and AWS Infrastructure

This post walks through the complete build and deployment of 86dfrom.com, a print-on-demand t-shirt storefront powered by Next.js 14, Printful APIs, and Stripe payments—deployed across Vercel and AWS CloudFront with DNS routing for multiple domain variants.

Project Architecture Overview

The 86dfrom project consists of three integrated systems:

  • Next.js 14 frontend + API routes: Hosted on Vercel, handles product catalog, cart logic, and Stripe webhook processing
  • Google Apps Script backend: Lightweight serverless compute for order confirmation emails and Sheets logging
  • AWS infrastructure: S3 + CloudFront for static assets and domain redirects, Route53 for DNS management

The project directory structure is:

/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│   ├── index.html          (static landing page)
│   └── success.html        (post-purchase confirmation)
├── gas/
│   ├── Code.gs            (Google Apps Script functions)
│   └── appsscript.json    (GAS project manifest)
├── scripts/
│   ├── deploy.sh          (S3 + CloudFront deployment)
│   └── get-printful-variants.js (fetch variant IDs from Printful API)
└── .env.local             (secrets: Stripe, Printful, webhook URLs)

Build & Compilation

The Next.js 14 build compiles cleanly with zero errors:

npm run build

This produces optimized bundles for five API routes:

  • /api/products — List Printful product variants
  • /api/checkout — Create Stripe checkout session
  • /api/webhook — Handle Stripe payment confirmations
  • /api/order — Trigger Google Apps Script order logging
  • /api/health — Health check for monitoring

All routes are TypeScript with strict type checking enabled.

Printful API Integration

We integrated Printful's REST API to dynamically fetch product variants. The script at scripts/get-printful-variants.js queries the Printful store (86Store) and extracts variant IDs for the Bella+Canvas 3001 Black unisex t-shirt across five sizes:

// Runs against the 86Store on the Hello Dangerous account
// Queries: GET https://api.printful.com/store/products
// Filters: Product ID 6482 (Bella+Canvas 3001), color Black, sizes XS–2XL
// Output: Five variant IDs stored in .env.local as comma-separated list

NEXT_PUBLIC_PRINTFUL_VARIANTS=4016,4017,4018,4019,4020

Why this approach? Hard-coding variant IDs would require manual updates whenever Printful's catalog changes. By fetching dynamically, the storefront stays in sync with Printful's product database. The script is idempotent and safe to run repeatedly.

Environment Configuration

The .env.local file contains three categories of configuration:

  • Printful credentials: API key and store ID
  • Stripe keys: Publishable key (frontend) and secret key (backend)
  • Webhook URLs: Google Apps Script deployment URL for order confirmations

We chose to deploy test mode first (sk_test_...) to validate the full payment flow before going live. This is critical: never move to live keys until end-to-end testing is complete.

Vercel Deployment

The Next.js application is deployed to Vercel Production:

npx vercel@latest --prod

This command:

  • Builds the project in Vercel's environment
  • Injects environment variables from .env.local into the production environment
  • Deploys to Vercel's edge network globally
  • Outputs a live URL (e.g., 86dfrom.vercel.app)

Critical: All environment variables must be added to Vercel's project settings before this step, or the build will fail during route compilation.

AWS Infrastructure: S3 + CloudFront + Route53

To handle the 86dfrom.com primary domain and 86from.com redirect domain, we provisioned AWS infrastructure:

S3 Buckets

Created: 86dfrom.com (primary static assets)

aws s3 mb s3://86dfrom.com --region us-east-1

Bucket policy restricts access to CloudFront only (via Origin Access Identity), preventing direct HTTP access to S3.

ACM Certificates

Requested two certificates in us-east-1 (CloudFront requirement):

  • 86dfrom.com (primary domain)
  • 86from.com (redirect domain)

Both use DNS validation via Route53, fully automated in our workflow. Certificates are auto-renewed by AWS.

CloudFront Distributions

Distribution 1: Primary (86dfrom.com)

  • Origin: S3 bucket 86dfrom.com
  • CNAME: 86dfrom.com
  • Certificate: ACM cert for 86dfrom.com
  • Cache behavior: 24-hour TTL for static assets, 0s for index.html
  • Custom error responses: 404 errors serve index.html (SPA routing support)

Distribution 2: Redirect (86from.com)

  • Origin: Dummy S3 bucket (not used for content)
  • Function: CloudFront Function intercepts all requests, returns HTTP 301 redirect to https://86dfrom.com$request_uri
  • CNAME: 86from.com
  • Certificate: ACM cert for 86from.com

Why two distributions? CloudFront cannot natively redirect traffic between CNAMEs. A CloudFront Function is the lightest-weight solution: it runs at edge locations with zero latency and handles the 301 redirect before any origin fetch occurs. The alternative (Lambda@Edge) would be overkill and more expensive.

Route53 DNS Records

Created two A records (alias) in the hosted zone for each domain:

86dfrom.com     A