Building a Printful-Integrated T-Shirt Store on Vercel: Infrastructure & Architecture for 86dfrom.com

What Was Done

We built a production-ready e-commerce site for 86dfrom.com that integrates Printful's on-demand printing API with Stripe payments, deployed on Vercel with CDN acceleration via CloudFront and S3. The project uses Next.js 14 with API routes for backend logic, environment-based configuration for multi-environment support, and AWS infrastructure for static asset hosting and distribution.

Project Structure & Technology Stack

The codebase at /Users/cb/Documents/repos/sites/86dfrom.com follows a standard Next.js monorepo pattern:

  • Frontend: site/index.html — Single-page app with vanilla JS + Tailwind CSS for responsive design
  • Backend APIs: pages/api/ — Five route handlers for Printful integration and Stripe webhooks
  • Google Apps Script: gas/Code.gs — Server-side form submission handler with validation
  • Infrastructure as Code: scripts/deploy.sh — Deployment automation for S3 + CloudFront invalidation
  • Configuration: .env.local — Environment variables for API credentials (Printful, Stripe, AWS)

The Next.js build compiles cleanly with zero warnings. All five API routes are verified to compile and route correctly.

Printful Integration Architecture

Rather than hard-coding product variant IDs, we implemented a script-based approach to fetch them dynamically from Printful's REST API. The file scripts/get-printful-variants.js queries Printful for the Bella+Canvas 3001 Black t-shirt (the core product) and extracts the five variant IDs (sizes XS–2XL, SKUs 4016–4020).

These variant IDs are then written to .env.local as comma-separated values under NEXT_PUBLIC_PRINTFUL_VARIANTS. The frontend site/index.html reads this at build time, and the /api/order endpoint uses these IDs to validate size selections before submitting orders to Printful.

Why this pattern? Printful frequently updates variant availability and pricing. By fetching variant IDs from the API rather than hardcoding them, we ensure the site always reflects the current store inventory without code changes.

Stripe Payment Flow & Webhook Strategy

The payment integration splits across two API routes:

  • /api/checkout — Creates a Stripe PaymentIntent and returns the client secret to the frontend
  • /api/webhook — Listens for payment_intent.succeeded events from Stripe's webhook service

The webhook handler at /api/webhook performs critical post-payment logic: it verifies the webhook signature (ensuring requests come from Stripe, not attackers), retrieves the PaymentIntent to confirm the amount, and then triggers the Printful order submission via /api/order.

This separation is intentional. Client-side requests from the browser cannot be trusted to submit orders to Printful; only after Stripe confirms payment server-side do we commit to the fulfillment partner. The webhook runs asynchronously and is idempotent (safe to retry), so if Printful fails, we can manually resubmit without double-charging.

Infrastructure: AWS + Vercel Hybrid Deployment

The site lives in two places for different purposes:

  • Vercel (Primary): Hosts the Next.js app at 86dfrom.vercel.app (or custom domain after DNS setup). All API routes run here.
  • AWS S3 + CloudFront (CDN): Serves static assets (fonts, images, CSS bundles) with low-latency global distribution.

The S3 bucket 86dfrom-com-assets was created with public-read ACM object policy. A CloudFront distribution (ID obtained via AWS CLI) was configured to:

  • Use the S3 bucket as the origin
  • Cache aggressively (1-year TTL for versioned assets)
  • Serve over HTTPS via an ACM certificate for 86dfrom.com
  • Invalidate via the deploy script when assets change

The scripts/deploy.sh script syncs the site/ directory to S3, then invalidates the CloudFront cache with aws cloudfront create-invalidation. This ensures users always see the latest assets without waiting for TTL expiration.

Environment Configuration Strategy

Rather than embedding secrets in code, we use Next.js conventions:

  • Public variables (prefixed NEXT_PUBLIC_): Variant IDs, publishable Stripe key, etc. Embedded in client-side bundles.
  • Server-only variables: Printful API key, Stripe secret key, webhook secret. Only available in Node.js runtime, never sent to the browser.

The .env.local file is never committed to version control. Instead, environment variables are set via:

  • Vercel's dashboard → Settings → Environment Variables (for production)
  • Local .env.local during development

This pattern prevents accidental credential leaks via GitHub or build logs.

Google Apps Script for Form Submissions

The gas/Code.gs file contains a simple form handler that processes submissions from site/index.html. Rather than sending data directly to a third-party service, it:

  • Validates email and size fields
  • Logs submissions to a Google Sheet (for admin visibility)
  • Returns a JSON response to the frontend

The appsscript.json manifest declares the script's OAuth scopes and web app permissions. The .clasp.json` file stores the GAS project ID for deployment via the clasp CLI.

Deployment Pipeline

The complete deployment follows this order:

  1. Environment Setup: Write .env.local with Printful and Stripe keys
  2. Build Verification: Run npm run build to confirm all routes compile
  3. Vercel Deploy: npx vercel@latest --prod to push to production
  4. Add Env Vars: Copy all .env.local variables to Vercel dashboard
  5. DNS Setup: Add Vercel's CNAME records to Route53 (or registrar DNS)
  6. Webhook Registration: In Stripe dashboard, point webhooks to https://86dfrom.com/api/webhook
  7. Static Assets: Run