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:

  1. Client calls POST /api/payment with cart items and customer email
  2. Server creates a Stripe PaymentIntent with metadata (order ID, Printful order details)
  3. Client confirms payment with confirmCardPayment(), including 3D Secure if required
  4. Stripe fires a payment_intent.succeeded webhook to POST /api/webhook
  5. 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.com with 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/