Homepage was rendering 71MB of HTML because every uploaded image (gallery, products, proofs, custom requests, design previews, invoices, receipts) was stored as base64 in Postgres and double-embedded via RSC hydration payloads. Adds src/lib/storage.ts (sharp-based compression, disk storage under public/uploads/) and a new /api/design-uploads endpoint for images added inside the design tool, migrates existing rows, and drops the now-unused base64 columns and the dead ProductImage table. Also fixes a systemic bug from the Next.js 16 upgrade where dynamic pages across the app still destructured params/searchParams synchronously instead of awaiting them as Promises, which was silently breaking filters/params and crashing /products/[slug] outright. Updates README.md and SETUP_AND_BUILD.md to reflect Postgres + Next.js 16 and document the new image storage architecture and backup requirements. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
16 KiB
Complete Setup & Build Guide
This document covers everything needed to develop, build, and deploy Craft2Prints.
Table of Contents
- Local Development Setup
- Configuration Reference
- Database Management
- Image & File Storage
- Feature Setup
- Building for Production
- Deployment
- Troubleshooting
Local Development Setup
Prerequisites
Node.js 18 or later is required:
node -v # Check your version
If not installed, download from https://nodejs.org (LTS version recommended).
PostgreSQL — the app connects to a Postgres database (prisma/schema.prisma's datasource is fixed to postgresql). Install it locally (https://www.postgresql.org/download/) or point DATABASE_URL at any reachable Postgres instance (a Docker container, a managed database, another machine on your network).
Step 1: Clone and Install
# Clone the repository
git clone <your-repo-url>
cd craft2prints
# Install dependencies
npm install
# This automatically runs: npx prisma generate
Step 2: Environment Setup
# Copy example environment file
cp .env.example .env
Edit .env and configure at minimum:
DATABASE_URL="postgresql://user:password@localhost:5432/craft2prints?schema=public"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
ADMIN_USER="admin"
ADMIN_PASSWORD="changeme"
ADMIN_SESSION_SECRET="<generate-random-string>"
SESSION_SECRET="<generate-random-string>"
For random secrets, use:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Step 3: Database Setup
# Create the database (make sure the DB named in DATABASE_URL already exists — e.g. `createdb craft2prints`)
# then sync the schema to it
npm run db:push
# Load starter products
npm run db:seed
Step 4: Start Development Server
npm run dev
Open http://localhost:3000 in your browser.
Auto-reload on file changes: The dev server watches your code and reloads automatically. If you change prisma/schema.prisma, run npm run db:push manually.
Configuration Reference
Essential Variables
| Variable | Description | Example |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string | postgresql://user:pass@host:5432/craft2prints?schema=public |
ADMIN_USER |
Admin login username | admin |
ADMIN_PASSWORD |
Admin login password | changeme |
ADMIN_SESSION_SECRET |
Session signing key (random) | 32-byte hex string |
SESSION_SECRET |
Customer session key (random) | 32-byte hex string |
Stripe Configuration
Get from https://dashboard.stripe.com (make sure you're in test mode first):
STRIPE_SECRET_KEY="sk_test_..." # From Developers > API keys
STRIPE_WEBHOOK_SECRET="whsec_..." # From Stripe CLI (see below)
Local Webhook Setup:
# Install Stripe CLI: https://docs.stripe.com/stripe-cli
stripe login
stripe listen --forward-to localhost:3000/api/webhook
# Copy the whsec_... secret into .env
Email (SMTP)
For order confirmations and password resets:
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="your-email@gmail.com"
SMTP_PASSWORD="your-app-password" # Not your normal password!
MAIL_FROM="Craft2Prints <your-email@gmail.com>"
ADMIN_NOTIFY_EMAIL="admin@example.com" # For photo request alerts
For Gmail: Generate an app password at https://myaccount.google.com/apppasswords (requires 2FA enabled).
CAPTCHA (Cloudflare Turnstile)
Protects public forms (registration, contact, custom requests):
NEXT_PUBLIC_TURNSTILE_SITE_KEY="..." # Get from dash.cloudflare.com
TURNSTILE_SECRET_KEY="..."
If not configured, forms work without CAPTCHA. Optional, not required.
Optional Variables
NEXT_PUBLIC_FACEBOOK_APP_ID="..." # For Messenger sharing
NEXT_PUBLIC_SITE_URL="http://localhost:3000" # For design approval links
Database Management
The app uses prisma db push (schema-driven sync), not versioned prisma migrate files — prisma/schema.prisma is the single source of truth for the database shape.
Common Tasks
Sync the schema to the database:
npm run db:push
Reset database (⚠️ deletes all data):
npx prisma db push --force-reset
npm run db:seed
Add new database tables/columns:
- Edit
prisma/schema.prisma - Run
npm run db:pushto sync
Because there's no migration history, renaming or dropping a column that already holds data is destructive — db push will refuse (or require --accept-data-loss) rather than silently drop it. For a column that needs to change shape on a live database, do it in three steps: add the new column alongside the old one (non-destructive db push), run a one-off script to copy/transform the data into it, update the app code to use the new column, then drop the old column in a second db push once you've confirmed nothing still depends on it.
Seed starter products:
npm run db:seed
Updating the seed is safe — it only touches the 6 starter products by their slug, leaving your custom products untouched.
Backup database:
pg_dump -h <host> -p <port> -U <user> -d craft2prints -F p -f "database-backups/craft2prints_$(date +%Y-%m-%d_%H-%M-%S).sql"
backup-database.ps1 in the repo root automates this — check its $dbHost/$dbPort/$dbUser match your actual DATABASE_URL before relying on it (it defaults to localhost:5432, which may not be where your database actually lives). Backups only cover the database — uploaded images/files live on disk under public/uploads/ and need their own backup (see Image & File Storage).
Image & File Storage
Every uploaded image or document — gallery photos, product photos, custom-request photos, design proofs, design-approval previews, order design previews, purchase invoices, expense receipts, and images added inside the design tool — is saved as a file on disk, not as base64 text in the database.
How it works
- A file arrives either as a
multipart/form-dataupload (admin forms, customer photo submissions) or as a base64 data URL generated client-side by the design tool's canvas export (stage.toDataURL()/canvas.toDataURL()). src/lib/storage.tsdecodes/receives it, and — unless it's a PDF — runs it throughsharpto resize (capped per category, never upscaled) and re-encode it (JPEG for photos, lossless PNG where transparency must be preserved, e.g. print-ready design previews).- The result is written to
public/uploads/<category>/<yyyy>/<mm>/<uuid>.<ext>and the database column stores only the resulting path, e.g./uploads/gallery/2026/07/3f9c1e2a-....jpeg.
Categories and their presets (IMAGE_PRESETS in src/lib/storage.ts):
| Category | Used for | Format | Max width |
|---|---|---|---|
gallery |
Public gallery photos | JPEG | 2000px |
products |
Product photos (main + per-color) | JPEG, or PNG if the source has transparency | 2000px |
custom-requests |
Customer-submitted photos | JPEG | 2000px |
proofs |
Admin-uploaded design proofs | JPEG | 2000px |
invoices / receipts |
Purchase/expense attachments | JPEG, or passthrough if PDF | 2000px |
designs |
Print-ready design previews (transparent) | PNG (lossless) | 4000px |
design-placements |
Design-on-product reference composites | JPEG | 2000px |
design-uploads |
Photos a customer adds inside the design tool | JPEG, or PNG if transparent | 3000px |
Why this matters operationally
public/uploads/is gitignored but lives permanently on the server's disk. It is not part of the database and is not covered by apg_dumpbackup — back it up separately (a simplersync/copy of the directory alongside your database backup schedule is enough).- No web server configuration is needed. Next.js serves everything under
public/automatically, in bothnext devandnext start— the same mechanism that already servespublic/logo.png. - On a fresh server, make sure the Node process can write to
public/uploads/(it's created automatically on first upload, but the parent directory must be writable). - Uploads endpoint for the design tool: images added inside the design canvas are uploaded via
POST /api/design-uploadsas soon as they're added, rather than embedded as base64 in the design's saved JSON — keeping cart/order/design-approval records small regardless of how many images a customer adds.
Troubleshooting
"sharp module could not be loaded" / native binding errors on the server
Sharp ships platform-specific native bindings — never copy node_modules between machines (e.g. from a Windows dev box to a Linux server). Run a fresh npm install on the target machine (the standard deploy flow below already does this).
Feature Setup
Stripe Payments
- Create account at https://dashboard.stripe.com
- Switch to Test Mode (top-right toggle)
- Get Secret Key from Developers > API keys
- Set
STRIPE_SECRET_KEYin.env - Start Stripe webhook listener (see Configuration Reference above)
- Test with card:
4242 4242 4242 4242, any future expiry, any 3-digit CVC
Email Notifications
Option 1: Gmail
- Enable 2-Factor Authentication
- Generate app password at https://myaccount.google.com/apppasswords
- Set SMTP variables in
.env - Test by submitting a form that triggers email
Option 2: Other Providers
- Outlook, SendGrid, AWS SES, etc. — check their SMTP settings
- Most follow the same pattern: username (email), password (app-specific)
CAPTCHA Protection
- Sign up at https://dash.cloudflare.com
- Go to Turnstile > Add Site
- Copy Site Key and Secret Key
- Set in
.env:NEXT_PUBLIC_TURNSTILE_SITE_KEY="..." TURNSTILE_SECRET_KEY="..." - Refresh the page — CAPTCHA should appear on forms
Building for Production
Build Process
# Build the Next.js app
npm run build
# Start the production server
npm start
The server listens on port 3000 by default.
Build Output
.next/ # Compiled Next.js app (production build)
.next/standalone/ # Self-contained server
Environment for Production
Before deploying, update .env for production:
# Database (use production connection string)
DATABASE_URL="postgresql://user:pass@prod-db:5432/craft2prints"
# SECURITY: Change these!
ADMIN_USER="<strong-username>"
ADMIN_PASSWORD="<strong-password>"
ADMIN_SESSION_SECRET="<new-random-string>"
SESSION_SECRET="<new-random-string>"
# Stripe (switch to LIVE keys, not test)
STRIPE_SECRET_KEY="sk_live_..."
STRIPE_WEBHOOK_SECRET="whsec_live_..."
# Site URL
NEXT_PUBLIC_SITE_URL="https://yourdomain.com"
# Email (production SMTP)
SMTP_HOST="..."
SMTP_USER="..."
SMTP_PASSWORD="..."
MAIL_FROM="Craft2Prints <noreply@yourdomain.com>"
Deployment
Option 1: Self-Hosted (Recommended for Control)
Hardware Requirements:
- Server with Node.js 18+ support
- 2GB+ RAM (more for concurrent traffic)
- A reachable PostgreSQL database (local install, Docker container, or managed service)
- Persistent disk for
public/uploads/— uploaded images/files live here, not in the database (see Image & File Storage)
Setup:
-
Upload code to server
scp -r . user@server:/app/craft2prints -
Install dependencies
ssh user@server cd /app/craft2prints npm install --production npm run build -
Set up environment
cp .env.example .env # Edit .env with production values nano .env -
Create database
npm run db:push npm run db:seed -
Install process manager (keep app running)
npm install -g pm2 pm2 start "npm start" --name craft2prints pm2 startup pm2 save -
Set up reverse proxy (nginx)
server { listen 80; server_name yourdomain.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } } -
Enable HTTPS (Let's Encrypt)
sudo apt-get install certbot python3-certbot-nginx sudo certbot --nginx -d yourdomain.com
Option 2: Platform Services
Vercel (easiest, free tier available)
- Push to GitHub
- Connect GitHub repo to Vercel
- Environment variables in Vercel dashboard
- Automatic deploys on push
Heroku (deprecated but still works)
- Use Procfile + buildpack
- Set environment variables in Heroku dashboard
Railway, Render, etc.
- Similar to Vercel — push → deploy
Option 3: Docker (Containerized)
Create Dockerfile:
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
docker build -t craft2prints .
docker run -p 3000:3000 --env-file .env -v craft2prints-uploads:/app/public/uploads craft2prints
The -v volume mount is required — without it, public/uploads/ lives inside the container's writable layer and every uploaded image/file is lost when the container is recreated.
Post-Deployment Checklist
- Admin password changed from default
- Session secrets regenerated
- Stripe keys switched to live (not test)
- Stripe webhook configured in dashboard
- HTTPS enabled with valid certificate
- Email SMTP tested (send test order confirmation)
- CAPTCHA working on public forms
- Database backups scheduled
public/uploads/backups scheduled (separate from the database — see Image & File Storage)- Log monitoring set up
- Rate limiting verified
- Staging environment created for testing
Troubleshooting
Development
"Cannot find module '@prisma/client'"
npx prisma generate
npm install
Port 3000 already in use
npm run dev -- -p 3001 # Use port 3001 instead
# Or find and kill process using port 3000
Blank product catalog
npm run db:seed # Load starter products
Database out of sync / corrupted state
npx prisma db push --force-reset # ⚠️ drops and recreates all tables
npm run db:seed
Production
App not starting
npm start # Check console for errors
npm run build # Rebuild if necessary
Email not sending
# Test SMTP configuration
node -e "const smtp = require('nodemailer'); console.log(JSON.stringify(process.env, null, 2))"
High memory usage
- Check for memory leaks:
node --inspect start - Increase server RAM or split into multiple processes
- Consider database optimization for large datasets
Database connection issues
# Test connection and list tables
psql $DATABASE_URL -c '\dt'
Maintenance
Regular Tasks
Daily:
- Monitor error logs
- Check payment processing (Stripe dashboard)
Weekly:
- Backup database and
public/uploads/ - Review admin activity logs
- Check storage usage
Monthly:
- Update npm dependencies:
npm outdated - Review security advisories:
npm audit - Analyze financial reports
Quarterly:
- Security audit
- Performance optimization
- Dependency updates
Updating
# Pull latest code
git pull
# Update dependencies
npm install
# Sync database schema
npm run db:push
# Rebuild if needed
npm run build
Security Hardening
Before Going Live
-
Change default credentials
- Admin username/password
- Session secrets (32-byte random strings)
-
Enable HTTPS
- Get certificate from Let's Encrypt (free)
- Set
NEXT_PUBLIC_SITE_URLtohttps://yourdomain.com
-
Rate Limiting
- Enabled by default on login/checkout
- Adjust in code if needed
-
CAPTCHA
- Recommended on all public forms
- Get free keys from Cloudflare Turnstile
-
Email
- Use app-specific passwords (not your actual password)
- Enable SMTP authentication
-
Backups
- Daily automated backups
- Test restore process quarterly
Support & Issues
- Questions? Check this guide again or search existing issues
- Bug report? Create GitHub issue with reproduction steps
- Feature request? Open an issue for discussion
Last updated: 2026-07-22