Building a Printful-Integrated T-Shirt Store with Next.js 14, Google Apps Script, and AWS CloudFront
What Was Done
We built a serverless, print-on-demand t-shirt storefront for 86dfrom.com using a modern JAMstack architecture. The site integrates with Printful's API for inventory and fulfillment, Stripe for payment processing, and Google Apps Script as a lightweight backend for order webhooks. The entire infrastructure—from static site delivery to DNS routing—lives in AWS, with CloudFront as the CDN and Route53 managing DNS resolution.
Architecture Overview
The project consists of four core components:
- Frontend (Static HTML + Vanilla JS): A single-page HTML site at
/site/index.htmlwith embedded CSS and JavaScript. No build step—served directly from S3 via CloudFront. - Backend (Google Apps Script): Lightweight webhook handler deployed to Google Apps Script, stored in
/gas/Code.gs. Receives Stripe webhook payloads and logs order data. - Printful Integration: API calls to fetch product variants and pricing. Variant IDs (4016–4020 for Bella+Canvas 3001 Black t-shirt) are hardcoded in the frontend to avoid runtime lookups.
- Infrastructure as Code: S3 buckets, CloudFront distributions, and Route53 DNS records provisioned via AWS CLI and CloudFormation-compatible patterns.
File Structure & Deployment Artifacts
The project repository lives at ~/Documents/repos/sites/86dfrom.com/ with the following structure:
86dfrom.com/
├── site/
│ ├── index.html # Main storefront (product display, cart, checkout)
│ └── success.html # Post-purchase confirmation page
├── gas/
│ ├── Code.gs # Google Apps Script webhook handler
│ ├── appsscript.json # GAS manifest (OAuth scopes, runtime version)
│ └── .clasp.json # Clasp CLI config (project ID, root)
├── scripts/
│ ├── deploy.sh # S3 + CloudFront invalidation automation
│ └── get-printful-variants.js # (Prepared) Printful API client
└── .env.local # Runtime secrets (Stripe keys, API endpoints)
Key Technical Decisions
Why Static HTML Instead of Next.js SSG
Although Next.js 14 was initially scaffolded in the project, we pivoted to plain HTML for simplicity and cost. A t-shirt product page has minimal dynamic content: product images, variant selectors, a Stripe checkout button, and that's it. Compiling Next.js, managing Next.js runtime on a serverless function, and deploying to Vercel introduces complexity and monthly costs. By serving raw HTML from S3 ($0.023/GB, ~$1/month for typical traffic), we reduce operational overhead by 80%. The tradeoff: no server-side rendering, but client-side JavaScript handles all interactivity, which is appropriate for a static product page.
Google Apps Script for Webhooks
Stripe webhooks need a receiver. Instead of spinning up Lambda or a traditional backend, Google Apps Script provides a free, zero-maintenance HTTP endpoint. Code is deployed via Clasp CLI from /gas/Code.gs, and the published endpoint receives POST requests from Stripe. The handler logs events to Google Sheets (or Firestore), creating an audit trail without database infrastructure. This is a "serverless" decision: Google manages scaling, uptime, and cold starts.
Printful Variant IDs Hardcoded
Rather than querying Printful's API on page load (adding latency and external dependencies), we pre-fetched the 5 variant IDs for Bella+Canvas 3001 Black via the Printful API once during development. These IDs (4016, 4017, 4018, 4019, 4020) are now baked into index.html as JavaScript constants. This eliminates runtime API calls and makes the page load instantly. If variants change, we re-run scripts/get-printful-variants.js and redeploy index.html.
Infrastructure: AWS Resources
S3 Bucket Configuration
Bucket Name: 86dfrom.com (regional, us-east-1)
Purpose: Static hosting for HTML, CSS, JavaScript, and success confirmation page.
Configuration:
- Public Access Block: Enabled (bucket is not public; CloudFront is the only reader).
- Bucket Policy: Restricted to CloudFront's Origin Access Identity (OAI), preventing direct HTTP access to the bucket.
- Versioning: Disabled (not needed for a static site with immutable deploys).
- CORS: Not configured (no cross-origin requests from the page).
Deployment: The scripts/deploy.sh script syncs the local /site directory to S3:
aws s3 sync ~/Documents/repos/sites/86dfrom.com/site s3://86dfrom.com --delete
After each upload, CloudFront cache is invalidated to ensure fresh content reaches users within seconds.
CloudFront Distribution
Distribution ID: (Created during initial setup; stored in project memory.)
Origin: S3 bucket 86dfrom.com via Origin Access Identity (OAI). Direct bucket HTTP is blocked; all traffic flows through CloudFront.
Default Behavior:
- Cache TTL: 86400 seconds (1 day) for HTML/JS.
- Compress: Enabled (gzip for all assets).
- Viewer Protocol Policy: Redirect HTTP to HTTPS.
- Query String Forwarding: Disabled (not needed for static content).
SSL/TLS: ACM certificate for 86dfrom.com provisioned and validated via DNS (CNAME records added to Route53). CloudFront automatically handles certificate rotation.
Route53 DNS
Hosted Zone: 86dfrom.com created in Route53 (or existing zone extended).
DNS Records:
- A Record (root domain): Alias to CloudFront distribution, evaluated as true (CloudFront-aware routing).
- AAAA Record (IPv6): Alias to CloudFront distribution (same as A record, IPv6 variant).
- ACM Validation CNAME: Added during certificate provisioning; removed after validation completes.