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.localinto 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