Building a Print-on-Demand T-Shirt Site with Next.js 14, Printful, and AWS Infrastructure
This post documents the full-stack implementation of 86dfrom.com, a print-on-demand t-shirt storefront built on Next.js 14, integrated with Printful's fulfillment API, and deployed across AWS CloudFront + S3 with Stripe payment processing. We'll cover the architecture decisions, infrastructure setup, and the integration patterns that tie everything together.
Project Structure and Build Setup
The project uses a clean Next.js 14 monorepo structure with the following layout:
86dfrom/
├── site/ # Static HTML landing pages
│ ├── index.html # Product showcase
│ └── success.html # Post-purchase confirmation
├── gas/ # Google Apps Script backend
│ ├── Code.gs # GAS deployment functions
│ └── appsscript.json # Project metadata
├── scripts/
│ ├── deploy.sh # AWS S3 + CloudFront deployment
│ └── get-printful-variants.js # Printful API integration
├── .env.local # Runtime secrets (generated)
└── package.json # Dependencies and build config
All five API routes compile cleanly without errors—the build system is production-ready. The Next.js application serves as the primary backend, handling Stripe webhook processing, Printful order creation, and inventory management.
Printful API Integration
Printful provides fulfillment services via their REST API. The integration centers on fetching product variant IDs, which map SKUs to printable items in Printful's system. The script scripts/get-printful-variants.js queries the Printful API to retrieve all available variants for the configured store.
We focused on a single product line: Bella+Canvas 3001 Black t-shirts. This product family includes five core variants:
- Variant ID 4016 (XS)
- Variant ID 4017 (S)
- Variant ID 4018 (M)
- Variant ID 4019 (L)
- Variant ID 4020 (XL)
These variant IDs are hardcoded into the pricing and inventory logic. When a customer selects a size, the frontend passes the variant ID to the backend, which uses it to create fulfillment orders in Printful. The Printful API key is stored in .env.local under the variable PRINTFUL_API_KEY.
Why Bella+Canvas 3001? It's a premium, unisex fit with excellent print durability and consistent sizing across variants. The black colorway eliminates color-matching complexity.
AWS Infrastructure: S3, CloudFront, and Route53
The static site files (HTML, CSS, fonts) are served through AWS CloudFront, backed by an S3 bucket. This provides global edge caching and HTTPS termination without managing servers.
S3 Bucket Configuration
An S3 bucket named 86dfrom-com stores the static assets. The bucket policy restricts access to CloudFront's Origin Access Identity (OAI), preventing direct S3 access and forcing all traffic through the CDN:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity [OAI-ID]"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::86dfrom-com/*"
}
]
}
CloudFront Distribution Setup
Two CloudFront distributions were created:
- 86dfrom.com distribution – Primary domain, serves the S3 bucket with the OAI. Configured with:
- Origin: S3 bucket
86dfrom-com.s3.us-west-2.amazonaws.com - Behavior: Cache static assets (HTML, CSS, images) with 24-hour TTL
- ACM certificate: Issued for
86dfrom.comand*.86dfrom.com - Origin access identity enabled
- Origin: S3 bucket
- 86from.com redirect distribution – Typo domain redirect. Uses a CloudFront Function to rewrite all requests:
- Function code redirects HTTP requests to
https://86dfrom.comwith 301 status - Prevents SEO fragmentation and typo traffic loss
- Function code redirects HTTP requests to
DNS Configuration
Route53 hosted zone for 86dfrom.com contains:
- A record:
86dfrom.com→ CloudFront distribution alias - A record:
www.86dfrom.com→ CloudFront distribution alias - CNAME records: ACM certificate validation (auto-generated, pointing to AWS validation endpoints)
- A record:
86from.com→ Redirect CloudFront distribution
ACM certificates were requested for both 86dfrom.com and 86from.com, with DNS validation records automatically added to Route53. This approach avoids email validation delays and ensures automatic renewal.
Deployment Pipeline
The scripts/deploy.sh script automates S3 + CloudFront deployment:
#!/bin/bash
# Push updated site/ to S3
aws s3 sync ./site/ s3://86dfrom-com/ --delete
# Invalidate CloudFront to force cache refresh
aws cloudfront create-invalidation \
--distribution-id [DISTRIBUTION-ID] \
--paths "/*"
This pattern is identical to the one used for dangerouscentaur.com deployments, ensuring consistency across projects. The --delete flag removes files from S3 that no longer exist locally, preventing orphaned assets.
Stripe Payment Integration
Stripe processes all payments. The client-side checkout form submits card data directly to Stripe via JavaScript, returning a payment intent ID. The backend verifies the payment intent and creates a Printful fulfillment order if successful.
Configuration is environment-based:
STRIPE_PUBLISHABLE_KEY– Public key for client-side token creation (test or live)STRIPE_SECRET_KEY– Secret key for server-side verification (test or live)STRIPE_WEBHOOK_SECRET– Webhook signing key (created after Vercel deployment when we register the webhook endpoint)
The webhook endpoint at /api/webhook listens for payment_intent.succeeded events and updates inventory in real-time.
Environment Configuration
The .env.local file (generated but not committed) contains all runtime secrets:
PRINTFUL_API_KEY=[api-key]