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-sitewith 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.comprovisioned and validated via DNS CNAME records in Route53 - Custom domain:
86dfrom.comaliased 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