Move uploaded images to disk storage, fix Next 16 async params/searchParams

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>
This commit is contained in:
Andymick
2026-07-22 20:56:40 +01:00
co-authored by Claude Sonnet 5
parent e23f201d3d
commit 396347b982
72 changed files with 1399 additions and 1336 deletions
+65 -22
View File
@@ -7,10 +7,11 @@ This document covers everything needed to develop, build, and deploy Craft2Print
1. [Local Development Setup](#local-development-setup)
2. [Configuration Reference](#configuration-reference)
3. [Database Management](#database-management)
4. [Feature Setup](#feature-setup)
5. [Building for Production](#building-for-production)
6. [Deployment](#deployment)
7. [Troubleshooting](#troubleshooting)
4. [Image & File Storage](#image--file-storage)
5. [Feature Setup](#feature-setup)
6. [Building for Production](#building-for-production)
7. [Deployment](#deployment)
8. [Troubleshooting](#troubleshooting)
---
@@ -25,6 +26,8 @@ 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
```bash
@@ -46,7 +49,7 @@ cp .env.example .env
Edit `.env` and configure at minimum:
```env
DATABASE_URL="file:./dev.db"
DATABASE_URL="postgresql://user:password@localhost:5432/craft2prints?schema=public"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
ADMIN_USER="admin"
ADMIN_PASSWORD="changeme"
@@ -62,7 +65,8 @@ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
### Step 3: Database Setup
```bash
# Create SQLite database and run migrations
# 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
@@ -87,7 +91,7 @@ Open http://localhost:3000 in your browser.
| Variable | Description | Example |
|----------|-------------|---------|
| `DATABASE_URL` | SQLite database path | `file:./dev.db` |
| `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 |
@@ -147,17 +151,18 @@ 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
**Create database and migrations:**
**Sync the schema to the database:**
```bash
npm run db:push
```
**Reset database (⚠️ deletes all data):**
```bash
rm dev.db
npm run db:push
npx prisma db push --force-reset
npm run db:seed
```
@@ -165,6 +170,8 @@ npm run db:seed
1. Edit `prisma/schema.prisma`
2. Run `npm run db:push` to 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:**
```bash
npm run db:seed
@@ -174,11 +181,47 @@ Updating the seed is safe — it only touches the 6 starter products by their sl
**Backup database:**
```bash
cp dev.db dev.db.backup-$(date +%Y%m%d-%H%M%S)
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)).
---
## 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
1. A file arrives either as a `multipart/form-data` upload (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()`).
2. `src/lib/storage.ts` decodes/receives it, and — unless it's a PDF — runs it through [`sharp`](https://sharp.pixelplumbing.com/) to 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).
3. 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 a `pg_dump` backup — back it up separately (a simple `rsync`/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 both `next dev` and `next start` — the same mechanism that already serves `public/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-uploads` as 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
@@ -274,7 +317,8 @@ MAIL_FROM="Craft2Prints <noreply@yourdomain.com>"
**Hardware Requirements:**
- Server with Node.js 18+ support
- 2GB+ RAM (more for concurrent traffic)
- SQLite storage, or PostgreSQL for larger deployments
- 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](#image--file-storage))
**Setup:**
@@ -366,8 +410,9 @@ CMD ["npm", "start"]
```bash
docker build -t craft2prints .
docker run -p 3000:3000 --env-file .env 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
@@ -379,6 +424,7 @@ docker run -p 3000:3000 --env-file .env craft2prints
- [ ] 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](#image--file-storage))
- [ ] Log monitoring set up
- [ ] Rate limiting verified
- [ ] Staging environment created for testing
@@ -406,10 +452,9 @@ npm run dev -- -p 3001 # Use port 3001 instead
npm run db:seed # Load starter products
```
**Database corrupted**
**Database out of sync / corrupted state**
```bash
rm dev.db
npm run db:push
npx prisma db push --force-reset # ⚠️ drops and recreates all tables
npm run db:seed
```
@@ -434,10 +479,8 @@ node -e "const smtp = require('nodemailer'); console.log(JSON.stringify(process.
**Database connection issues**
```bash
# Test connection
psql $DATABASE_URL # If PostgreSQL
# Or
sqlite3 dev.db ".tables" # If SQLite
# Test connection and list tables
psql $DATABASE_URL -c '\dt'
```
---
@@ -451,7 +494,7 @@ sqlite3 dev.db ".tables" # If SQLite
- Check payment processing (Stripe dashboard)
**Weekly:**
- Backup database
- Backup database and `public/uploads/`
- Review admin activity logs
- Check storage usage
@@ -521,4 +564,4 @@ npm run build
---
*Last updated: 2025-03-17*
*Last updated: 2026-07-22*