Building a Printful + Stripe T-Shirt Store on Next.js 14: Infrastructure & Deployment Patterns
This post documents the end-to-end setup of 86dfrom.com, a t-shirt e-commerce site powered by Printful's print-on-demand API, Stripe payments, and Next.js 14. We'll cover the architecture decisions, infrastructure setup on AWS/Cloudflare, and the deployment pipeline that ties it all together.
Project Structure & Technology Stack
The project is organized into three core directories:
/site— Static HTML landing page and success confirmation page/gas— Google Apps Script backend for webhook handling (alternative runtime)/scripts— Deployment and data-fetching utilities
The Next.js application itself lives at the repository root, with five API routes handling the full transaction lifecycle:
/api/products— Fetch Printful product variants and pricing/api/cart— Calculate totals and apply discount logic/api/order— Create Printful orders and trigger Stripe payment/api/webhook— Handle Stripepayment_intent.succeededwebhooks/api/fulfill— Status polling and shipment tracking (optional async route)
The build was verified clean with next build, confirming all routes compile without errors.
API Integration & Environment Configuration
Three external APIs drive the system: Printful, Stripe, and Google Apps Script (for webhook resilience).
Printful API Setup: The store account "86Store" under the "Hello Dangerous" workspace uses token-based authentication. A utility script at /scripts/get-printful-variants.js queries the Printful API to extract variant IDs for the Bella+Canvas 3001 Black t-shirt:
// Fetch variant data from Printful
fetch('https://api.printful.com/products', {
headers: { Authorization: 'Bearer PRINTFUL_API_TOKEN' }
})
.then(r => r.json())
.then(data => {
// Extract variant IDs for Black colorway (SKU variants 4016-4020)
// Output as .env variable
})
The script populates variant IDs into .env.local, which the Next.js API routes consume at runtime. This decouples product data from code and allows live variant updates without redeployment.
Stripe Integration: The order creation route (/api/order) follows the standard Stripe Payment Intent pattern:
- Client sends cart data to
/api/order - Backend calculates total and creates a Printful order
- Backend initializes a Stripe
PaymentIntentwithclient_secret - Client uses
confirmPayment()on the secret to finalize payment - Stripe sends
payment_intent.succeededwebhook to/api/webhook - Webhook updates order status and sends confirmation email
This pattern ensures idempotency: if a webhook retries, the order record already exists and duplicate charges are prevented.
Infrastructure: DNS, CDN, and SSL
The domain 86dfrom.com uses a hybrid AWS + Vercel setup:
- DNS: Route53 hosted zone for
86dfrom.com - Web: Vercel (Next.js app, API routes, webhooks)
- Static Assets: S3 + CloudFront (optional; fonts, images)
- SSL: ACM certificates for both
86dfrom.comand86from.com(domain variant)
S3 Bucket Configuration: Bucket 86dfrom-static stores the landing page HTML and success page. Bucket policy restricts access to CloudFront only, preventing direct S3 URL exposure:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::86dfrom-static/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::ACCOUNT_ID:distribution/DISTRIBUTION_ID"
}
}
}]
}
CloudFront Distributions: Two distributions manage traffic:
- Primary (86dfrom.com): Origin points to S3 bucket. Caches
/site/*with 1-hour TTL. Origins set to use Origin Access Identity (OAI) for secure S3 access. - Redirect (86from.com): Uses CloudFront Functions to redirect all requests to
https://86dfrom.com. The function (deployed viascripts/deploy-redirect.js) intercepts incoming requests and returns a 301 redirect response.
Route53 A records alias both distributions to their respective CloudFront domain names, eliminating the need for intermediate CNAME setup.
Certificate Validation: ACM certificates for both domains were requested and validated via DNS CNAME records added to Route53. This allows automatic renewal without manual intervention.
Deployment Pipeline
Vercel Deployment: The Next.js application deploys to Vercel via npx vercel@latest --prod. Environment variables are set in the Vercel project settings before deployment:
NEXT_PUBLIC_STRIPE_PK— Stripe publishable key (client-side safe)STRIPE_SK— Stripe secret key (server-side only)PRINTFUL_API_TOKEN— Printful API bearer tokenPRINTFUL_STORE_ID— Numeric store ID from Printful accountPRINTFUL_PRODUCT_ID— Product ID for the Bella+Canvas shirtVARIANT_IDS— Comma-separated list of 5 variant IDs (output fromget-printful-variants.js)STRIPE_WEBHOOK_SECRET— Webhook signing secret (added after deployment)
The .env.local file is never committed to git. It's created locally and referenced only during development. Production secrets are managed exclusively through Vercel's UI or CLI.
Static Asset Deployment: The /scripts/deploy.sh bash script automates S3 + CloudFront updates:
#!/bin/bash
# Copy site files to S3
aws s3 cp site/ s3://86