Building a Printful-Integrated T-Shirt Store on Next.js 14: Infrastructure and Deployment Strategy
What Was Done
We built a full-stack e-commerce application for 86dfrom.com—a Printful-integrated t-shirt store powered by Next.js 14, Stripe payments, and Google Apps Script webhooks. The project involved scaffolding a clean Next.js application, configuring environment variables across development and production, provisioning AWS S3 and CloudFront infrastructure, setting up DNS across Route53, and deploying to Vercel with integrated Stripe payment processing.
Project Structure and Architecture
The codebase follows a modular Next.js 14 structure:
/Users/cb/Documents/repos/sites/86dfrom.com/
├── site/
│ ├── index.html (landing page / product showcase)
│ └── success.html (post-purchase confirmation)
├── gas/
│ ├── Code.gs (Google Apps Script webhook handler)
│ └── appsscript.json (GAS manifest and clasp config)
├── scripts/
│ └── deploy.sh (S3 + CloudFront deployment automation)
├── .env.local (local development secrets)
└── [Next.js app routes] (API routes, pages, components)
This hybrid structure serves both static assets (via CloudFront) and dynamic API endpoints (via Vercel). The separation allows us to maintain a fast CDN for marketing assets while keeping compute-intensive operations (Stripe validation, Printful API calls) in a serverless environment.
Environment Configuration Strategy
We implemented a three-layer environment setup:
- Development (
.env.local): Sensitive keys stored locally, git-ignored, includes Printful API token, Stripe test/live keys, and webhook secrets - Vercel Production: Environment variables configured via Vercel dashboard, automatically injected at build/runtime
- Static Assets (S3/CloudFront): No secrets required; signed URLs handled server-side via API routes
This approach isolates secrets from version control while enabling secure deployment across environments. The Stripe webhook endpoint at /api/webhook receives signed payloads, validates them using the webhook secret, and updates order state without exposing keys in transit.
Infrastructure: AWS and DNS Configuration
S3 Bucket for Static Assets
We created a dedicated S3 bucket (86dfrom-site-assets) to host static marketing assets, product images, and downloadables. The bucket is configured with:
- Block Public Access: Enabled (no direct public access)
- CloudFront Origin Access Identity (OAI): Bucket policy restricts access to CloudFront distribution only
- Versioning: Enabled for audit trail and rollback capability
- Server-Side Encryption: AES-256 default
The bucket policy allows only the CloudFront OAI to read objects, preventing direct S3 URL access and forcing all traffic through the CDN for caching, compression, and DDoS protection.
CloudFront Distribution
Two CloudFront distributions were provisioned:
- Primary Distribution (
86dfrom.com):- Origin: S3 bucket with OAI
- Default TTL: 3600 seconds (1 hour) for index.html, 31536000 (1 year) for versioned assets
- Compress: Enabled (Gzip + Brotli)
- HTTP/HTTPS: HTTPS enforced
- SSL Certificate: AWS Certificate Manager (ACM) wildcard for *.86dfrom.com
- Redirect Distribution (
86from.com):- CloudFront Functions: Evaluates requests to
86from.com, returns HTTP 301 redirect tohttps://86dfrom.com - Purpose: Capture typo traffic and legacy domain references
- CloudFront Functions: Evaluates requests to
The redirect function is lightweight—a single CloudFront Functions script (not Lambda@Edge) that executes at edge locations with minimal latency. This pattern prevents content duplication and centralizes SEO value to the canonical domain.
Route53 DNS and SSL
Route53 hosts the domain with the following record structure:
86dfrom.com(A record): Alias to CloudFront distributionwww.86dfrom.com(CNAME): Points to primary CloudFront distribution86from.com(A record): Alias to redirect CloudFront distribution- ACM Validation CNAMEs: DNS validation records for both domains' SSL certificates
We used Route53 alias records (not CNAME) for apex domains—this avoids the CNAME limitation at zone apex and keeps routing logic within AWS.
Vercel Deployment and API Routes
The Next.js application is deployed to Vercel with the following structure:
app/
├── page.tsx (homepage)
├── api/
│ ├── webhook/ (Stripe webhook handler)
│ ├── checkout/ (Stripe Checkout session creation)
│ └── variants/ (Printful variant ID endpoint)
└── layout.tsx (root layout)
Key API routes:
POST /api/checkout: Accepts product selection, calls Stripe API to create a Checkout session, returns session ID for client redirectPOST /api/webhook: Validates incoming Stripe events using webhook secret, processespayment_intent.succeededevents, queues fulfillment to Google Apps ScriptGET /api/variants: Returns cached Printful variant IDs for Bella+Canvas 3001 Black t-shirts (variants 4016–4020)
We chose Vercel for its native Next.js optimization, built-in environment variable injection, and automatic HTTPS. The serverless function cold-start latency is acceptable for checkout flows where users expect a brief redirect delay.
Printful Integration
Printful API calls are made server-side to avoid exposing the API key in client-side code. The integration fetches product variants on deployment (via a build-time script) and caches them in .env.local:
NEXT_PUBLIC_VARIANTS_4016=Bella+Canvas+Black+S
NEXT_PUBLIC_VARIANTS_4017=Bella+Canvas+Black+M
NEXT_PUBLIC_VARIANTS_4018=Bella+Canvas+Black+L
# etc.
The NEXT_PUBLIC_ prefix means these values are exposed to the browser (safe because they're not credentials—just product metadata). The actual Printful API key (PRINT