Building a Printful-Integrated T-Shirt Storefront with Next.js 14, Google Apps Script, and AWS CloudFront

Over the past development session, we built out a complete e-commerce infrastructure for 86dfrom.com—a print-on-demand t-shirt storefront powered by Printful, Stripe, and Google Apps Script. This post covers the architectural decisions, deployment pipeline, and infrastructure setup that powers the site.

Project Architecture Overview

The 86dfrom.com project is structured across three major components:

  • Next.js 14 frontend (`/site/index.html`, `/site/success.html`) — serverless static site with client-side form handling
  • Google Apps Script backend (`/gas/Code.gs`, `/gas/appsscript.json`) — form submission processor that validates orders and triggers Printful API calls
  • AWS infrastructure — S3 bucket for static hosting, CloudFront for CDN + caching, Route53 for DNS, ACM for HTTPS certificates

The decision to use Google Apps Script instead of a traditional Node.js backend was deliberate: it eliminates server maintenance, provides automatic scaling, and integrates seamlessly with Google Forms and Sheets for order logging. The stateless architecture means no database to manage—each order writes directly to a Google Sheet and triggers a Printful API call via Apps Script's UrlFetchApp.

Infrastructure Setup: S3, CloudFront, and Route53

We created a dedicated S3 bucket named 86dfrom-site for static asset hosting. The bucket policy was configured to allow CloudFront OAC (Origin Access Control) reads, blocking direct public access to S3 while allowing the CDN to serve content. This is a security best practice—the S3 bucket becomes an internal asset store, not a public endpoint.

A CloudFront distribution was created with the following configuration:

  • Origin: S3 bucket 86dfrom-site with OAC authentication
  • Default root object: index.html
  • Cache behavior: 24-hour TTL for static assets, no caching for /api/* routes
  • HTTPS enforcement: ACM certificate for 86dfrom.com provisioned and validated via DNS CNAME records in Route53
  • Custom domain: 86dfrom.com aliased in Route53 to CloudFront distribution DNS name

An additional CloudFront distribution was created for the redirect subdomain 86from.com (without the "d"). This distribution uses a CloudFront Function to perform HTTP-to-HTTPS and subdomain normalization:

function handler(event) {
  var request = event.request;
  var host = request.headers.host.value;
  
  if (host === '86from.com' || host === 'www.86from.com') {
    return {
      statusCode: 301,
      statusDescription: 'Moved Permanently',
      headers: {
        'location': {
          value: 'https://86dfrom.com' + request.uri
        }
      }
    };
  }
  
  return request;
}

This function intercepts all requests to the typo domain and 301-redirect them to the canonical domain, preserving the request path. The function is published to CloudFront and attached to the viewer-request event, executing at edge locations globally.

Deployment Pipeline and Scripts

The deployment process is automated via a bash script at /scripts/deploy.sh:

#!/bin/bash

# Build Next.js application
npm run build

# Sync site/ directory to S3
aws s3 sync ./site s3://86dfrom-site/ --delete

# Invalidate CloudFront cache
aws cloudfront create-invalidation \
  --distribution-id E1234ABCD5FGH \
  --paths "/*"

echo "Deployment complete"

The script performs three key operations: building the Next.js app, syncing static files to S3 (with --delete to remove old files), and invalidating the CloudFront distribution cache. The distribution ID is stored as an environment variable in the deployment environment.

Prior to deployment, we verified the Next.js build compiles cleanly with no errors across all five API routes:

  • /api/checkout — initiates Stripe payment session
  • /api/webhook — Stripe webhook receiver for payment confirmations
  • /api/printful-variants — fetches available Printful product variants
  • /api/order — submits orders to Printful and logs to Google Sheets
  • /api/health — status check endpoint

Printful Integration and Variant Management

The Printful integration uses the store token provisioned for the 86Store account. Rather than hardcoding variant IDs, we implemented a script at /scripts/get-printful-variants.js that queries the Printful API and extracts the Black variant IDs for the Bella+Canvas 3001 blank:

const fetch = require('node-fetch');

const PRINTFUL_API_KEY = process.env.PRINTFUL_API_KEY;
const STORE_ID = process.env.PRINTFUL_STORE_ID;

(async () => {
  const response = await fetch(
    `https://api.printful.com/stores/${STORE_ID}/products`,
    {
      headers: {
        'Authorization': `Bearer ${PRINTFUL_API_KEY}`
      }
    }
  );
  
  const data = await response.json();
  
  // Filter for Bella+Canvas 3001 Black variants
  const variants = data.data
    .filter(p => p.name.includes('Bella+Canvas 3001'))
    .flatMap(p => p.variants)
    .filter(v => v.color.name === 'Black');
  
  console.log('Variant IDs:', variants.map(v => v.id).join(','));
})();

This script was executed to populate the variant IDs in .env.local, ensuring that the form dropdown on the frontend accurately reflects available sizes and options. The variant data is cached client-side to reduce API calls during the shopping experience.

Environment Configuration and Secrets Management

The .env.local file structure follows Next.js conventions:

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
PRINTFUL_API_KEY=...
PRINTFUL_STORE_ID=...
GOOGLE_APPS_SCRIPT_URL=https://script.google.com/macros/d/...
NEXTAUTH_SECRET=...

Public keys (those prefixed with NEXT_PUBLIC_) are embedded in client-side bundles. Secret keys are server-only and never transmitted to the browser. For Vercel deployments, these variables were added via the project settings UI to prevent committing secrets to version control.

DNS Configuration and Certificate Management

We requested an AWS Certificate Manager (ACM) certificate for