```html

Building a Printful-Integrated T-Shirt Store on Vercel: From Local Development to Production Deployment

Over the past development session, we built out a complete production-ready t-shirt e-commerce site for 86dfrom.com, integrating Printful's API for on-demand fulfillment, Stripe for payments, and Google Apps Script for order management. This post covers the architectural decisions, deployment pipeline, and infrastructure setup that took the project from zero to live.

Project Overview

The 86dfrom.com site is a Next.js 14 application that allows users to customize and purchase t-shirts. The architecture consists of:

  • Frontend: Next.js 14 with API routes for backend logic
  • Fulfillment: Printful API integration for real-time variant data and order submission
  • Payments: Stripe for payment processing and webhook handling
  • Order Management: Google Apps Script for serverless order storage and notifications
  • Hosting: Vercel for the web application, CloudFront + S3 for static assets and redirects

Development Environment Setup

The project structure mirrors production deployments we've done for other brands under the dangerouscentaur umbrella:

~/Documents/repos/sites/86dfrom.com/
├── site/
│   ├── index.html           # Landing page (static alternative)
│   └── success.html         # Order confirmation
├── gas/
│   ├── Code.gs              # Google Apps Script for order webhook
│   └── appsscript.json      # GAS configuration
├── scripts/
│   ├── deploy.sh            # S3 + CloudFront deployment
│   └── get-printful-variants.js  # Fetch Printful variant IDs
└── .env.local               # Local secrets (gitignored)

We set up .env.local with three critical values:

  • NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY – Stripe public key for client-side payment UI
  • STRIPE_SECRET_KEY – Stripe secret key for server-side payment processing
  • PRINTFUL_API_KEY – Printful authentication for variant and order APIs

The publishable key is prefixed with NEXT_PUBLIC_, making it available to the browser. The secret keys stay server-side only, enforced by Next.js's build-time constant stripping.

Printful Integration Strategy

Rather than hardcoding product variant IDs, we built a script-based approach to fetch them dynamically. The script scripts/get-printful-variants.js authenticates to Printful, queries the product catalog, and extracts variant IDs for Bella+Canvas 3001 Black t-shirts across all sizes (XS through 3XL).

Variant IDs from Printful are numeric and tied to their internal product database. By storing these in environment variables, we can swap product variants without redeploying the application—useful if we need to adjust quality, supplier, or color mid-campaign.

The API integration points are:

  • /api/routes/variants – Returns cached variant data
  • /api/routes/order – Submits orders to Printful and Stripe
  • /api/webhook – Receives Stripe payment confirmations

Stripe Payment Flow

The payment architecture follows Stripe's recommended server-side confirmation pattern:

  1. Client submits order details (size, quantity, customer info) to /api/routes/order
  2. Backend creates a Stripe Payment Intent with the order total
  3. Backend submits order to Printful with Stripe payment intent ID as idempotency key
  4. Client receives client secret and completes payment via Stripe Elements
  5. Stripe confirms the charge and posts to /api/webhook
  6. Webhook triggers Google Apps Script to log order and send confirmation email

This flow decouples Stripe payment confirmation from Printful order submission, letting us retry Printful orders if the API is temporarily unavailable without double-charging the customer.

Google Apps Script for Order Fulfillment

The gas/Code.gs file implements a webhook receiver that Google Sheets can trigger. When Stripe confirms a payment, the webhook calls the GAS deployment URL with order metadata. The script:

  • Appends order data to a Google Sheet for auditing
  • Sends a formatted email notification to the store owner
  • Logs timestamps for SLA tracking

We configured appsscript.json with "runtimeVersion": "V8" to enable modern JavaScript (arrow functions, async/await) and set the deployment as a web app with "execute as" set to the service account owner.

AWS Infrastructure: S3 + CloudFront

Beyond the Vercel application, we deployed static content and DNS redirects using AWS:

  • S3 Bucket: 86dfrom.com – Stores static HTML (index.html, success.html)
  • CloudFront Distribution for 86dfrom.com: Aliases 86dfrom.com and www.86dfrom.com, with S3 origin and index document routing
  • CloudFront Distribution for 86from.com (redirect): Implements a CloudFront Function to 301-redirect all requests to https://86dfrom.com
  • Route53 Hosted Zone: Manages DNS for both 86dfrom.com and 86from.com with A/AAAA alias records pointing to CloudFront distributions

The S3 bucket policy enforces read-only access via CloudFront's Origin Access Control (OAC), preventing direct S3 access. The CloudFront redirect function (deployed via the AWS console) intercepts requests to 86from.com and returns a 301 redirect, allowing us to consolidate traffic on the primary domain.

Build Pipeline and Deployment

The Next.js build was verified clean with no compilation errors:

npx next build
# Output: ✓ Route /
# Output: ✓ Route /api/routes/variants
# Output: ✓ Route /api/routes/order
# Output: ✓ Route /api/webhook
# Output: ✓ Route /success

We deployed to Vercel using:

npx vercel@latest --prod

Post-deployment, we configured environment variables in the Vercel project settings, ensuring they're available to all function runtimes. The STRIPE_SECRET_KEY and PRINTFUL_API_KEY are marked as sensitive and never logged.

The scripts/deploy.sh script automates S3 uploads and CloudFront invalidation:

#!/bin/bash
aws s3 sync site/ s3://86dfrom.com --delete
aws cloudfront create-invalidation --distribution-id [ID