Building a Printful-Integrated T-Shirt E-Commerce Site with Next.js 14, Google Apps Script, and AWS CloudFront
This post documents the full-stack development and deployment of 86dfrom.com, a print-on-demand t-shirt storefront powered by Printful's API, Stripe payments, and a serverless backend. We'll cover the architecture, deployment pipeline, and why certain technical decisions were made.
Project Overview
The goal was to build a lightweight, fast e-commerce site that:
- Displays Bella+Canvas 3001 black t-shirts with dynamic variant selection
- Integrates with Printful's REST API to fetch real-time product variants
- Processes payments via Stripe with webhook validation
- Serves both a public storefront and a success confirmation page
- Maintains a Google Apps Script backend for order fulfillment logging
Tech Stack
- Frontend: Next.js 14 (App Router), deployed on Vercel
- Backend APIs: Next.js API routes (
/app/api/) - Order Fulfillment: Google Apps Script (GAS) with
appsscript.json` manifest - Payment Processing: Stripe (REST API + webhooks)
- Product Data: Printful API (variant IDs, pricing)
- DNS & CDN: Route53 + CloudFront
- Static Assets: S3 (future static site fallback)
Directory Structure
/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│ ├── index.html (legacy static fallback)
│ └── success.html (order confirmation page)
├── gas/
│ ├── Code.gs (Google Apps Script functions)
│ ├── appsscript.json (GAS manifest with Sheets bindings)
│ └── .clasp.json (clasp CLI config for GAS deployment)
├── scripts/
│ └── deploy.sh (S3 + CloudFront invalidation script)
├── .env.local (runtime secrets: Stripe, Printful, GAS webhook)
└── [Next.js app files]
Core Architecture Decisions
Why Printful API Over Manual Inventory?
Printful's REST API provides real-time variant availability and pricing without maintaining our own database. We fetch variant IDs (e.g., 4016 for XS Black, 4017 for S Black) once at build time, then store them in .env.local. This avoids:
- Database latency for simple product data
- Manual SKU management across multiple print providers
- Inventory sync complexity
A simple scripts/get-printful-variants.js` fetches variant metadata from Printful's API and prints them to stdout, which we then hardcode into environment variables.
Why Google Apps Script for Order Logging?
Instead of a traditional database, we use Google Apps Script bound to a Google Sheet. When a Stripe webhook fires at /api/webhook, the Next.js backend invokes the GAS webhook URL (stored in .env.local as GOOGLE_APPS_SCRIPT_WEBHOOK_URL) with order details. GAS appends a row to the sheet, creating an audit trail without server infrastructure.
Why this pattern?
- No database to provision or monitor
- Familiar spreadsheet interface for non-technical team members
- Built-in email notifications via Google Sheets
- Version history and access controls are free
Why Stripe Webhooks Over Polling?
Rather than polling Stripe's API for payment status, we register a webhook endpoint at /api/webhook in the Stripe dashboard. Stripe sends a POST request with event data (e.g., payment_intent.succeeded) as soon as the payment completes. This:
- Reduces API calls and latency
- Guarantees delivery (with exponential backoff retries)
- Allows synchronous order logging before the customer sees a success page
Infrastructure & Deployment
DNS & SSL
The domain 86dfrom.com is managed in AWS Route53. We requested an ACM certificate for 86dfrom.com with DNS validation, which required adding a CNAME record in Route53 pointing to AWS's validation endpoint. Once validated, the certificate is used by CloudFront.
Similarly, 86from.com (no 'd') was set up as a redirect distribution: a CloudFront function at the viewer-request stage intercepts traffic and redirects to 86dfrom.com.
CloudFront Distribution
The main CloudFront distribution for 86dfrom.com`:
- Origin: Vercel deployment (CNAME:
86dfrom-com.vercel.appor direct Vercel domain) - SSL Certificate: ACM certificate for
86dfrom.com`- Cache Policy: Managed-CachingOptimized for static assets (JS, CSS, images)
- Origin Request Policy: AllViewerExceptCloudFrontHeaders (forwards cookies and authorization headers to Vercel)
- Viewer Protocol Policy: Redirect HTTP to HTTPS
Why CloudFront in front of Vercel?
- Edge caching of static assets (reduced origin requests)
- Custom domain with our own SSL certificate
- CloudFront functions for lightweight request transformation (like the 86from.com redirect)
- DDoS protection via AWS Shield Standard
S3 Bucket (Fallback)
A static S3 bucket was provisioned (86dfrom-com-s3) as a potential fallback and for hosting the legacy HTML files. Its bucket policy restricts access to CloudFront only, preventing direct public access. The scripts/deploy.sh script copies site/index.html and site/success.html to S3 and invalidates the CloudFront cache:
#!/bin/bash
aws s3 cp site/ s3://86dfrom-com-s3/ --recursive
aws cloudfront create-invalidation \
--distribution-id E2ABC123EXAMPLE \
--paths "/*"
Route53 Records
Two A records were created in Route53 for 86dfrom.com`:
- Alias to CloudFront Distribution: Points
86dfrom.com` to the main distribution (no origin IP needed; Route53 automatically resolves the distribution's domain)