Building a Printful-Integrated T-Shirt Store with Next.js 14, Google Apps Script, and AWS CDN
This post documents the infrastructure and application setup for 86dfrom.com, a print-on-demand t-shirt store integrating Printful's API, Stripe payments, and a custom Google Sheets inventory management backend. The project demonstrates how to bridge serverless compute, static CDN distribution, and third-party fulfillment APIs into a cohesive production system.
Project Architecture Overview
The 86dfrom.com stack consists of three primary layers:
- Frontend: Next.js 14 static site deployed to Vercel with API routes for Stripe webhook handling
- Fulfillment API: Printful integration via REST API for variant lookups and order routing
- Data Backend: Google Apps Script bound to a Google Sheet, handling inventory updates and order metadata
- Payment Processing: Stripe integration with webhook verification for order confirmation
This architecture separates concerns: the Next.js application handles user-facing commerce and payment orchestration, while Google Apps Script manages internal order state and Google Sheets serves as the source of truth for inventory status.
Next.js 14 Project Structure and Build Configuration
The project scaffolding follows Next.js 14 conventions with explicit API route separation:
86dfrom/
├── app/
│ ├── page.tsx (homepage with product display)
│ ├── success/
│ │ └── page.tsx (post-purchase confirmation)
│ └── api/
│ ├── checkout/route.ts (Stripe Checkout Session creation)
│ ├── webhook/route.ts (Stripe event listener)
│ └── variants/route.ts (Printful variant enumeration)
├── public/
│ └── (fonts, images, static assets)
├── .env.local (environment secrets)
├── package.json
└── next.config.js
The Next.js build produces static HTML and optimized JavaScript bundles. We verified the production build with npm run build, which confirmed all 5 routes compile cleanly with zero errors. This is critical for Vercel deployment—any TypeScript or compilation errors would block production release.
Printful API Integration: Variant ID Resolution
Printful's API requires exact product variant IDs to create orders. Rather than hardcoding these IDs, we created a variant discovery script:
scripts/get-printful-variants.js
This script authenticates to the Printful API using a store-level bearer token and queries the product catalog. For the 86Store (a sub-store within the dangerouscentaur.com Printful account), we targeted the Bella+Canvas 3001 t-shirt in black colorway, which maps to variant IDs 4016–4020 (covering size XS through 2XL).
Why variant IDs matter: Printful's order API requires explicit variant IDs to route fulfillment requests. A single product (e.g., "Bella 3001 Black") has multiple variants (one per size). Hardcoding these is fragile; a store migration or product swap would break orders. The script automates discovery, making it safe to update the product catalog without code changes.
The script output populates .env.local:
NEXT_PUBLIC_PRINTFUL_VARIANT_XS=4016
NEXT_PUBLIC_PRINTFUL_VARIANT_S=4017
NEXT_PUBLIC_PRINTFUL_VARIANT_M=4018
NEXT_PUBLIC_PRINTFUL_VARIANT_L=4019
NEXT_PUBLIC_PRINTFUL_VARIANT_XL=4020
Note the NEXT_PUBLIC_ prefix: these values are safe to expose in the frontend, as they're product identifiers, not secrets.
Stripe Payment Flow and Webhook Security
The payment flow involves two API routes:
/api/checkout— receives POST requests with size and quantity, creates a Stripe Checkout Session, and redirects the browser to Stripe-hosted checkout/api/webhook— listens for Stripe events (specificallycheckout.session.completed), verifies the webhook signature, and triggers order fulfillment to Printful and inventory updates to Google Sheets
Webhook security is non-negotiable. Stripe signs each webhook with an HMAC-SHA256 signature using a secret key. The route validates this signature before processing:
const sig = request.headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
This prevents malicious actors from spoofing order completion events. The webhook secret is environment-only (not in code) and rotated via Stripe's dashboard.
Google Apps Script Backend for Order State
The gas/Code.gs file contains the Apps Script entry point. This script is deployed as a web app (published with "Execute as" set to the service account, "Allow" set to "Anyone") and listens for POST requests from the Stripe webhook handler.
The Apps Script:
- Receives order metadata (size, quantity, Stripe session ID) from the webhook route
- Appends a row to a Google Sheet named "Orders" with timestamps and status
- Optionally triggers email notifications or inventory decrement logic
The deployment is managed via the Clasp CLI, stored in gas/.clasp.json, and deployed to a specific Google Apps Script project ID. This separation keeps backend logic out of Next.js, reducing bundle size and enabling independent scaling of the inventory system.
Infrastructure: Vercel Deployment and Environment Variables
The Next.js app is deployed to Vercel, which provides:
- Global CDN for static assets and HTML
- Serverless function execution for API routes
- Automatic HTTPS and domain management
- Environment variable management via the Vercel dashboard
Critical environment variables configured in Vercel (not in .env.local):
STRIPE_SECRET_KEY— Stripe secret for Checkout Session creation and webhook verificationSTRIPE_WEBHOOK_SECRET— HMAC key for webhook signature validationPRINTFUL_API_KEY— Bearer token for Printful API callsGOOGLE_APPS_SCRIPT_URL— Deployed Apps Script web app URL for order notifications
The deployment command:
npx vercel@latest --prod
This reads the Vercel project configuration from .vercel/project.json and pushes the build to production. DNS records for 86dfrom.com point to Vercel's nameservers, enabling instant HTTPS and global distribution.
DNS and Domain Configuration
The domain 86dfrom.com is managed via Route53 (AWS). The DNS setup requires:
- CNAME record pointing the domain to Vercel's edge