Building a Printful T-Shirt Store on Next.js 14 with Stripe & AWS CloudFront Distribution
This post documents the technical implementation of 86dfrom.com, a Printful-integrated e-commerce site built on Next.js 14, deployed across Vercel (frontend) and AWS (static assets), with Stripe payment processing and CloudFront CDN distribution.
Architecture Overview
The project uses a hybrid deployment strategy:
- Frontend: Next.js 14 app deployed to Vercel (handles API routes, webhooks, dynamic content)
- Static Assets: S3 bucket (
86dfrom.com) served via CloudFront distribution - Print-on-Demand: Printful API integration for variant management and order fulfillment
- Payments: Stripe for checkout, with webhook listener at
/api/webhook - DNS: Route53 for domain management and CloudFront alias records
Project Structure
The repository at ~/Documents/repos/sites/86dfrom.com/ contains three main directories:
86dfrom.com/
├── site/ # Static HTML, CSS, assets
│ ├── index.html # Main product page
│ └── success.html # Post-purchase confirmation
├── gas/ # Google Apps Script (optional backend)
│ ├── Code.gs
│ ├── appsscript.json
│ └── .clasp.json
├── scripts/
│ └── deploy.sh # S3 + CloudFront invalidation
└── .env.local # Environment variables (Printful API, Stripe keys)
The Next.js app itself is configured with:
next.config.js # Image optimization, redirects
pages/api/
├── webhook.ts # Stripe webhook endpoint
└── printful/
└── variants.ts # Variant data endpoint
Printful Integration
To populate Bella+Canvas 3001 Black t-shirt variants, we run a script that fetches variant IDs from the Printful API. The Printful account is registered under "Hello Dangerous" (dangerouscentaur.com) with a store named 86Store.
The API key has full scopes across all stores. Rather than hardcoding variant IDs, we call:
scripts/get-printful-variants.js
This script queries the Printful REST API endpoint:
GET https://api.printful.com/store/products
We filter the response for product ID 264 (Bella+Canvas 3001) and extract variant IDs for the Black color only (variant IDs 4016–4020 in size range XS–2XL). We ignore heather and oxblood variants to keep the product selection focused.
The variant IDs are stored in .env.local:
NEXT_PUBLIC_PRINTFUL_VARIANT_IDS=4016,4017,4018,4019,4020
PRINTFUL_API_KEY=UPQNIqzJkpoV2JPKKrhwYteCKzhipRnLHA2TxLnt
The NEXT_PUBLIC_ prefix makes these IDs available to browser-side code for real-time Printful API calls; the secret key stays server-only.
AWS Infrastructure: S3 + CloudFront + Route53
S3 Bucket Configuration
We created a dedicated S3 bucket named 86dfrom.com with the following policy:
- Block Public Access: Disabled (allowing CloudFront to read objects)
- Bucket Policy: Restricts access to CloudFront origin access control (OAC), preventing direct S3 URL access
- Content Types: HTML served with
text/html, CSS withtext/css, woff2 fonts withfont/woff2 - Cache Headers: Static assets set to 1-year expiry; HTML set to 1-hour revalidation
CloudFront Distributions
Two CloudFront distributions were created:
- Primary distribution (86dfrom.com)
- Origin: S3 bucket
86dfrom.com - Origin Access Control: Enabled (restricts direct S3 access)
- Default root object:
index.html - Behaviors: Cache optimized for static content (HTML, CSS, fonts)
- SSL certificate: AWS Certificate Manager (ACM) certificate for
86dfrom.com
- Origin: S3 bucket
- Redirect distribution (86from.com)
- CloudFront function deployed to redirect HTTP traffic from
86from.comtohttps://86dfrom.com - Avoids typo-domain confusion; catches users who omit the "d"
- CloudFront function deployed to redirect HTTP traffic from
ACM Certificate & DNS Validation
We requested ACM certificates for both 86dfrom.com and 86from.com (the redirect domain). AWS generates CNAME records that must be added to Route53 to validate ownership:
_abcd1234.86dfrom.com. CNAME _efgh5678.acm-validations.aws.
_ijkl9012.86from.com. CNAME _mnop3456.acm-validations.aws.
These records are automatically verified within 5–10 minutes. Certificates are valid for 1 year and auto-renewed by AWS.
Route53 DNS Records
In the Route53 hosted zone for 86dfrom.com, we added:
86dfrom.com. A alias [CloudFront primary distribution domain]
86from.com. A alias [CloudFront redirect distribution domain]
Both are alias records (not CNAMEs) pointing to their respective CloudFront distributions, with Evaluate Target Health enabled.
Deployment Pipeline
The deployment script (scripts/deploy.sh) handles S3 sync and CloudFront cache invalidation:
#!/bin/bash
aws s3 sync ./site s3://86dfrom.com --delete
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"
This ensures:
- All files in
./site/are uploaded to S3 - Deleted local files are removed from the bucket (using
--delete) - CloudFront cache is invalidated for
/*(all paths)
The script is run after each content update, ensuring new assets are live