Building a Serverless T-Shirt Print-on-Demand Site: Next.js 14, Google Apps Script, and AWS Infrastructure
This post documents the complete infrastructure and architecture built for 86dfrom.com, a print-on-demand t-shirt storefront using Printful fulfillment, Stripe payments, and a hybrid serverless stack spanning Vercel, Google Apps Script, and AWS.
Project Overview
The 86dfrom project is a full-stack e-commerce site designed to minimize operational overhead while maintaining a professional user experience. The architecture leverages:
- Frontend: Next.js 14 (App Router) deployed on Vercel
- Backend: Next.js API routes for Stripe webhook handling and Printful integration
- Fulfillment: Printful API for inventory and order routing
- Order persistence: Google Apps Script with Sheets as a lightweight database
- CDN/DNS: AWS CloudFront + Route53 for domain management
- Static assets: Amazon S3 with CloudFront caching
Directory Structure and File Organization
The project is organized into three primary directories under ~/Documents/repos/sites/86dfrom.com/:
86dfrom.com/
├── site/ # Static assets and fallback HTML
│ ├── index.html # Main landing/product page
│ └── success.html # Post-purchase confirmation page
├── gas/ # Google Apps Script backend
│ ├── Code.gs # Main GAS functions for Sheets integration
│ ├── appsscript.json # GAS project manifest
│ └── .clasp.json # Clasp CLI config for deployments
└── scripts/
└── deploy.sh # Bash deployment script for S3/CloudFront
This structure separates static content (served via CloudFront from S3), the Next.js application (Vercel), and the Apps Script backend (Google Cloud) into distinct deployment targets.
Printful Integration: Dynamic Variant Loading
Rather than hardcoding product variant IDs, the project implements a dynamic variant loader that queries Printful's API. The approach:
- Store Setup: Created a Printful store called
86Storeunder the dangerouscentaur.com account, scoped with full API permissions. - Variant Discovery: The script
scripts/get-printful-variants.js(executed once during setup) fetches all SKUs for the base product (Bella+Canvas 3001 Black t-shirt) and extracts variant IDs for sizes XS through 3XL. - Hardcoded Registry: Variant IDs (4016–4020) are stored in
.env.localas environment variables, mapped to sizes in the Next.js route handler atapp/api/products/route.ts.
This design avoids runtime Printful API calls on the critical path (product listing), instead using Printful only for order submission via the webhook endpoint.
Stripe Payment Flow and Webhook Architecture
The payment pipeline follows a standard e-commerce pattern:
Frontend (Stripe.js)
→ Payment Intent created via POST /api/payment
→ Stripe processes card
→ Webhook POST to /api/webhook (Stripe event)
→ Orders routed to Printful
→ Order data logged to Google Sheets
The Stripe webhook endpoint at app/api/webhook/route.ts is configured with a webhook secret (obtained post-deployment in Vercel's environment settings). It:
- Validates the HMAC signature of the incoming POST request using the raw request body and the stored
STRIPE_WEBHOOK_SECRET. - Listens for
payment_intent.succeededevents only. - Calls the Google Apps Script
doPostfunction via HTTPS to persist order metadata (customer email, size, color, address). - Simultaneously submits the order to Printful's `/api/v2/orders` endpoint using the Printful API key.
The critical decision here was not storing order data in a traditional database. Instead, Google Sheets acts as the single source of truth for order history, eliminating the need for a separate backend database while remaining queryable and auditable.
Google Apps Script Backend
The file gas/Code.gs defines two core functions:
doPost(e): Accepts JSON payloads from the Vercel webhook handler, parses order details, and appends rows to a Google Sheet namedOrders. The sheet is shared with the dangerouscentaur.com account for visibility.doGet(e): (Optional) Provides a lightweight REST endpoint for fetching order summaries, useful for admin dashboards.
The appsscript.json manifest specifies runtime configuration and OAuth scopes required to read/write Sheets. Deployment is handled via the clasp CLI (Google Apps Script Clasp), which authenticates via .clasp.json and pushes code to the GAS project ID tied to the dangerouscentaur account.
AWS Infrastructure: S3, CloudFront, and Route53
Two AWS resources were created to support the 86dfrom domain:
Primary S3 Bucket and CloudFront Distribution
- S3 Bucket:
86dfrom.com(region: us-east-1) holds static assets:site/index.html,site/success.html, and CSS/font assets. - Bucket Policy: Allows CloudFront origin access identity (OAI) to read all objects, blocking direct HTTP access.
- CloudFront Distribution: Created with the S3 bucket as origin, SSL/TLS certificate (requested via AWS Certificate Manager for
*.86dfrom.comand86dfrom.com), and caching behaviors: - HTML files: TTL 1 hour (cache busting on redeployment)
- CSS/fonts: TTL 1 year (immutable assets)
- Default root object:
index.html
Redirect CloudFront Distribution for Alternate Domain
A second CloudFront distribution was created to handle requests to 86from.com (missing the 'd'), redirecting via CloudFront Functions to the canonical domain. This is a low-cost mitigation for typos and brand protection.
Route53 Configuration
Two A records (alias records) were added to the Route53 hosted zone for dangerouscentaur.com:
86dfrom.com → CloudFront distribution alias
86from.com → Redirect CloudFront distribution alias
The ACM certificates for both domains were validated using