Building a Printful-Integrated T-Shirt Commerce Site: Next.js 14, Google Apps Script, and AWS Infrastructure
This post details the complete build of 86dfrom.com, a print-on-demand t-shirt storefront integrating Printful's API, Stripe payments, and a custom backend powered by Google Apps Script. We'll walk through the architecture decisions, infrastructure setup, and deployment pipeline that makes this work.
Project Architecture Overview
The site is built as a Next.js 14 full-stack application with the following structure:
- Frontend: React components handling product display, variant selection, and checkout flow
- Backend: Next.js API routes for Printful integration, Stripe webhook handling, and order management
- Fulfillment: Google Apps Script deployed as a standalone web app, acting as a webhook receiver for order confirmation emails and analytics
- Deployment: Vercel for the Next.js application; S3 + CloudFront for static assets and domain serving
This hybrid approach allows us to leverage Printful's print-on-demand infrastructure while maintaining full control over the customer experience and payment flow.
Environment Configuration and Build Validation
The project was scaffolded with a clean Next.js 14 build. Before deployment, we verified compilation of all five core routes:
/– Product listing and variant selector/checkout– Stripe payment integration/success– Order confirmation page/api/printful– Variant and pricing proxy from Printful/api/webhook– Stripe webhook receiver for order status updates
All routes compiled without errors, confirming the foundation was solid before integrating external services.
Printful API Integration Strategy
Rather than hardcoding variant IDs, we implemented a script-based workflow to fetch them dynamically. The Printful store 86Store (within the dangerouscentaur.com account) was configured with a full-scope API token. Here's the workflow:
# Pseudocode for scripts/get-printful-variants.js
const store_id = process.env.PRINTFUL_STORE_ID;
const api_key = process.env.PRINTFUL_API_KEY;
// Query Printful for all products
const products = await fetch(`https://api.printful.com/store/${store_id}/products`, {
headers: { Authorization: `Bearer ${api_key}` }
});
// Filter for Bella+Canvas 3001 Black t-shirts (SKU variants 4016–4020)
const variants = products
.filter(p => p.sku.match(/^(4016|4017|4018|4019|4020)$/))
.map(v => ({
id: v.id,
size: v.size,
price: v.retail_price
}));
// Write to .env.local for build-time consumption
fs.writeFileSync('.env.local', `NEXT_PUBLIC_VARIANTS=${JSON.stringify(variants)}`);
This approach decouples product data from code, making future size/color additions trivial—just re-run the script and redeploy.
Infrastructure: S3, CloudFront, and Route53
The domain 86dfrom.com was set up with AWS infrastructure mirroring our existing pattern (used on chuckladd.com and dangerouscentaur.com):
S3 Bucket Creation
Created bucket 86dfrom.com in us-east-1 with the following policy:
- Block all public access disabled (allows CloudFront origin access)
- Bucket policy grants
s3:GetObjectto CloudFront Origin Access Identity (OAI) - Static website hosting disabled (CloudFront is the sole entry point)
ACM Certificate
Requested a public ACM certificate for 86dfrom.com in us-east-1 with DNS validation. The validation CNAME records were added to Route53 hosted zone 86dfrom.com, issued under the same account. Certificate status was polled until ISSUED before proceeding to CloudFront distribution creation.
CloudFront Distribution
Created distribution d3hk7qx2m9v4r1.cloudfront.net (example ID) with:
- Origin:
86dfrom.com.s3.amazonaws.comwith OAI - Default Root Object:
index.html - Viewer Protocol Policy: Redirect HTTP to HTTPS
- Cache Behavior: 24-hour TTL for HTML; longer for static assets
- Alternative Domain Names (CNAME):
86dfrom.comandwww.86dfrom.com - SSL Certificate: ACM certificate created above
Once the distribution was deployed, Route53 A-record alias was created pointing 86dfrom.com to the CloudFront distribution.
Wildcard Subdomain Redirect (86from.com)
A secondary domain 86from.com (typo variant) was registered and configured to redirect all traffic to 86dfrom.com`. This involved:
- Creating a second ACM certificate for
86from.com` - Creating a CloudFront function (Lambda@Edge alternative) to redirect all requests to
https://86dfrom.com$uri` - Deploying a lightweight CloudFront distribution purely for redirect logic
- Adding Route53 A-record alias pointing
86from.comto the redirect distribution
This catches typos and user-entered URLs without domain aliases or nginx configuration.
Google Apps Script Backend
For email notifications and order logging, we deployed a Google Apps Script web app deployed at /Users/cb/Documents/repos/sites/86dfrom.com/gas/Code.gs:
// Example Apps Script webhook handler
function doPost(e) {
const payload = JSON.parse(e.postData.contents);
if (payload.type === 'stripe.charge.succeeded') {
sendOrderConfirmationEmail(payload.data);
logToSheet(payload);
return HtmlService.createTextOutput('OK').setMimeType(HtmlService.MimeType.TEXT);
}
}
function sendOrderConfirmationEmail(charge) {
GmailApp.sendEmail(
charge.billing_details.email,
'Your 86dfrom order is confirmed',
`Order ID: ${charge.id}\nTracking will update within 24 hours.`
);
}
The Apps Script project is configured in appsscript.json with OAuth scopes for Gmail and Google Sheets. Deployment URL is stored in `.env.local` as NEXT_PUBLIC_GAS_WEBHOOK_URL.