Building a Printful + Stripe T-Shirt Site on Next.js 14: Infrastructure, API Integration, and Deployment Strategy
This post documents the complete technical setup for 86dfrom.com, a Next.js 14 e-commerce application that integrates Printful's print-on-demand API with Stripe payment processing. The project demonstrates a modern serverless architecture combining AWS infrastructure with Vercel deployment, emphasizing clean separation of concerns and environment-driven configuration.
Architecture Overview
The 86dfrom project follows a monorepo structure with three distinct concerns:
- Next.js Frontend (
/app) — Product pages, checkout UI, order confirmation - API Routes (
/app/api) — Printful variant fetching, Stripe payment intents, webhook processing - Google Apps Script Backend (
/gas) — Serverless order persistence and email notifications
This hybrid approach leverages Vercel's edge network for the customer-facing application while using Google Sheets as a lightweight database for order records—a pragmatic choice for low-volume, high-margin product sales.
Project Structure and Configuration
The deployed structure looks like this:
86dfrom.com/
├── site/ # Static assets deployed to S3 (legacy/redirect)
├── gas/ # Google Apps Script backend
│ ├── Code.gs # Main Apps Script functions
│ └── appsscript.json # GAS manifest with scopes
├── scripts/
│ └── deploy.sh # CloudFront invalidation script
└── .env.local # Local environment variables (not in git)
The Next.js application structure mirrors standard Next.js 14 conventions with App Router:
app/
├── page.tsx # Home / product listing
├── checkout/page.tsx # Checkout flow
├── success/page.tsx # Post-purchase confirmation
└── api/
├── printful/route.ts # GET /api/printful → variant IDs
├── payment/route.ts # POST /api/payment → Stripe intent
└── webhook/route.ts # POST /api/webhook → Stripe events
Environment Configuration and Secrets Management
Rather than hardcoding credentials, the project uses environment variables managed through:
.env.local(development) — Contains Printful API key, Stripe keys, GAS webhook URL- Vercel Environment Variables (production) — Same variables synced via
npx vercel env pull - Stripe Webhook Secret — Generated post-deployment, added only to production
This pattern ensures local development mirrors production behavior without storing secrets in version control.
Printful API Integration
The Printful integration serves two purposes: variant enumeration at build time and order creation at runtime.
A script at scripts/get-printful-variants.js runs during development to fetch available product variants from Printful's /api/v2/products endpoint. The script targets the Bella+Canvas 3001 Black t-shirt (Printful product ID determined via dashboard), extracting variant IDs for sizes XS through 3XL:
// Pseudo-code: Printful variant fetching
const response = await fetch('https://api.printful.com/api/v2/products', {
headers: {
'Authorization': `Bearer ${PRINTFUL_API_KEY}`,
},
});
const variants = response.data
.filter(v => v.product_id === BELLA_CANVAS_3001)
.map(v => ({ size: v.size, variant_id: v.id }));
Variant IDs are hard-coded into the product configuration (or fetched at runtime via /api/printful), ensuring the UI never references an invalid SKU. This is critical because Printful's API throws errors for non-existent variants, which would break the checkout flow.
Stripe Payment Processing
Stripe integration uses the Payment Intents API for PCI-DSS compliance and idempotency:
- Client calls
POST /api/paymentwith cart items and customer email - Server creates a Stripe
PaymentIntentwith metadata (order ID, Printful order details) - Client confirms payment with
confirmCardPayment(), including 3D Secure if required - Stripe fires a
payment_intent.succeededwebhook toPOST /api/webhook - Webhook handler verifies signature, creates Printful order, records in Google Sheets
The webhook endpoint (/api/webhook) is critical: it must verify the Stripe signature using the webhook secret to prevent forged payment confirmations. This is non-negotiable for security.
Infrastructure: AWS, CloudFront, and DNS
While the primary application runs on Vercel, the project also maintains a parallel AWS infrastructure for redundancy and asset delivery:
- S3 Bucket:
86dfrom.com— Stores static HTML, CSS, and redirects - CloudFront Distribution (production) — CDN for
86dfrom.comwith cache invalidation on deploy - CloudFront Distribution (redirect) — Routes legacy
86from.com(typo domain) to primary domain - ACM Certificates — TLS for both domains, auto-renewed
- Route53 Hosted Zone — DNS records pointing to CloudFront distributions
The S3 bucket policy restricts public access; only CloudFront's origin access identity (OAI) can read objects. This prevents direct S3 access and ensures all traffic flows through the CDN for caching and DDoS protection.
Deployment Pipeline
Two deployment flows exist:
Vercel (Primary):
npx vercel@latest --prod
# Automatically:
# 1. Builds Next.js app
# 2. Uploads to Vercel's global edge network
# 3. Runs on serverless functions
# 4. Sets DNS via Vercel nameservers (if configured)
S3 + CloudFront (Secondary):
bash scripts/deploy.sh
# Manually:
# 1. Uploads site/ directory to S3
# 2. Invalidates CloudFront cache (/* pattern)
# 3. Distributes updates globally within 60 seconds
The S3 deployment is idempotent—re-running the script doesn't duplicate objects, only overwrites changes. CloudFront invalidation uses the /* wildcard, ensuring all cached assets are refreshed on deploy.
Google Apps Script Integration
The /gas/Code.gs file defines two key functions:
doPost(e)— Webhook receiver; parses Stripe/