```html

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:

  1. Store Setup: Created a Printful store called 86Store under the dangerouscentaur.com account, scoped with full API permissions.
  2. 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.
  3. Hardcoded Registry: Variant IDs (4016–4020) are stored in .env.local as environment variables, mapped to sizes in the Next.js route handler at app/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.succeeded events only.
  • Calls the Google Apps Script doPost function 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 named Orders. 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.com and 86dfrom.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