Building a Printful-Integrated T-Shirt Commerce Site on Vercel: From Next.js 14 to CloudFront CDN
What Was Done
We built a full-stack e-commerce site for 86dfrom.com, a print-on-demand t-shirt storefront powered by Printful's API. The architecture spans three environments: local development on Next.js 14, serverless functions on Vercel, and static assets cached through AWS CloudFront with S3 origins. The site handles product variant fetching, real-time inventory from Printful, Stripe payment processing, and webhook-driven order fulfillment.
Project Structure & File Organization
The project lives at /Users/cb/Documents/repos/sites/86dfrom.com with this layout:
86dfrom.com/
├── site/ # Static HTML (Google Apps Script web app)
│ ├── index.html # Landing page with product gallery
│ └── success.html # Post-purchase confirmation
├── gas/ # Google Apps Script backend
│ ├── Code.gs # GAS functions (deprecated — replaced by Vercel)
│ └── appsscript.json # GAS manifest
├── scripts/
│ ├── deploy.sh # S3 + CloudFront invalidation script
│ └── get-printful-variants.js # Fetch variant IDs from Printful API
├── .env.local # Secrets: Stripe keys, Printful token
└── .clasp.json # GAS project credentials (legacy)
The decision to move from Google Apps Script to Vercel was driven by the need for reliable webhook handling for Stripe payments. GAS has request timeouts and limited concurrency; Vercel's serverless architecture provides better scalability and native Node.js runtime for payment processing.
Infrastructure: AWS + Vercel + Printful
S3 & CloudFront Setup
Static assets (HTML, CSS, fonts) are served from S3 bucket 86dfrom-site, cached behind CloudFront distribution E2ABCD1234EFGH (example ID). This provides:
- Low latency: Global edge locations cache site HTML and the Anton font (woff2 format from Google Fonts)
- Cost efficiency: Data transfer from S3 to CloudFront is free; users pay only for edge egress
- Cache invalidation: The
scripts/deploy.shscript uploads new HTML, then runsaws cloudfront create-invalidationon the distribution to bust cache
An additional CloudFront distribution handles the legacy domain redirect (86from.com → 86dfrom.com) using CloudFront Functions, a lightweight alternative to Lambda@Edge for simple URL rewrites.
Route53 is configured with:
- A-record:
86dfrom.com→ CloudFront distribution CNAME - ALIAS record:
www.86dfrom.com→ same distribution (for redundancy) - CNAME record:
_abc123.86dfrom.com→ ACM DNS validation (certificate issued for wildcard * + apex)
Vercel Deployment
The Next.js 14 app is deployed to Vercel with this structure:
app/
├── page.jsx # Homepage
├── api/
│ ├── variants.js # GET /api/variants — returns Printful data
│ ├── checkout.js # POST /api/checkout — creates Stripe session
│ ├── webhook.js # POST /api/webhook — Stripe webhook handler
│ └── printful-sync.js # Cron job to refresh inventory
├── layout.jsx
└── globals.css
Environment variables are stored in Vercel's dashboard under Settings → Environment Variables:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY— Public key for frontend checkoutSTRIPE_SECRET_KEY— Secret key (only backend)STRIPE_WEBHOOK_SECRET— Webhook signing key (set after DNS validation)PRINTFUL_API_KEY— OAuth token for store accessPRINTFUL_STORE_ID— Numeric ID of the 86Store
Printful API Integration
The scripts/get-printful-variants.js` script queries the Printful API to fetch product variant IDs:
#!/usr/bin/env node
// scripts/get-printful-variants.js
const https = require('https');
const apiKey = process.env.PRINTFUL_API_KEY;
const storeId = process.env.PRINTFUL_STORE_ID;
https.get(`https://api.printful.com/stores/${storeId}/products`, {
headers: { 'Authorization': `Bearer ${apiKey}` }
}, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
const products = JSON.parse(data).result;
products.forEach(p => {
console.log(`Product: ${p.name}`);
p.variants.forEach(v => {
console.log(` Variant ${v.id}: ${v.title}`);
});
});
});
});
We fetch the Bella+Canvas 3001 black t-shirt in sizes XS–2XL (variant IDs 4016–4020). These IDs are hard-coded in app/api/variants.js and used to create Stripe checkout sessions with correct pricing and inventory.
Payment Flow: Stripe Integration
The checkout flow is:
- User selects size on
/, clicks "Buy" - Frontend POST to
/api/checkoutwith variant ID app/api/checkout.jscalls Stripe API to create a checkout session, including Printful's SKU and price- User redirected to Stripe's hosted checkout page
- Post-purchase, Stripe sends webhook to
/api/webhookwith event typecheckout.session.completed - Webhook handler verifies signature (using
STRIPE_WEBHOOK_SECRET), then triggers Printful order creation
The webhook signing verification is critical: Stripe uses HMAC-SHA256 to sign payloads. The stripe.webhooks.constructEvent() function verifies this signature before processing, preventing replay attacks and spoofed events.
Key Architectural Decisions
Why Vercel over Google Apps Script?
GAS was the initial choice for simplicity (no DevOps), but it has hard limits:
- 6-minute request timeout (Stripe webhooks may retry after 10+ seconds)
- 1 concurrent execution per user (serializes requests)
- No native HTTPS POST for webhook signature verification in the way Stripe requires
Vercel solves