Sharp 0.34 ships a separate prebuilt binary per platform tuple as
optionalDependencies (@img/sharp-linuxmusl-x64 for Alpine, -darwin-arm64
for Apple Silicon, etc).
The lock file only records the dev machine's platform binary. `npm ci`
is strict — it installs exactly what's locked and refuses to add
platform-specific entries that weren't there at lock time. Result on
the VPS Alpine x64 build:
sharp: Attempting to build from source via node-gyp
sharp: Please add node-gyp to your dependencies
Fix: after the strict `npm ci`, run a separate `npm install --no-save
--cpu=x64 --os=linux --libc=musl sharp`. This downloads the Alpine
binary into node_modules without modifying package.json or the lock,
so the dev's Mac install stays untouched on the next git pull.
This is the recommended pattern from sharp's own docs for cross-target
Docker builds: https://sharp.pixelplumbing.com/install#cross-platform
Two related fixes for the deploy pipeline so DB schema changes never
again leave the site half-deployed.
PRISMA CONFIG (prisma.config.ts)
- "import 'dotenv/config'" was hard-required, but dotenv isn't installed
in the production runtime image (env vars come from docker-compose).
- Wrapped in try/catch so it loads .env locally and silently no-ops in
the container — `prisma migrate deploy` works in both environments.
DOCKERFILE
- Copies node_modules/prisma + prisma.config.ts to the runner stage so
the CLI is available at runtime, not just at build.
- New CMD runs `prisma migrate deploy` before booting the server.
Idempotent — already-applied migrations are skipped. If the DB is
unreachable, the container exits and docker-compose retries.
- This means: from now on, `git pull && docker compose up -d --build app`
is the entire deploy. No more "did you remember to run migrations?".
DEFENSIVE TRY/CATCH (applications/[slug]/page.tsx)
- prisma.application.findUnique and prisma.globalNode.findMany now have
try/catch with logged errors. A transient DB hiccup or missing
Application slug now degrades gracefully (renders "not found" or empty
cases wall) instead of triggering a 500 Internal Server Error.
DEPLOY (David, this is the recovery sequence on the VPS)
cd /opt/flux-srl
git pull
docker compose up -d --build app
# The container will run pending migrations on its own.
# No need to run `prisma migrate deploy` manually anymore.
Two production-grade hardening additions and one cost optimisation.
FLUXAI AUTONOMY RESTORED (api/chat)
- Brings back the multi-step agentic flow that the system prompt was
always designed for. The "temporarily removed maxSteps" comment is
gone — replaced with the AI SDK 6 equivalent stopWhen: stepCountIs(5).
- Cap at 5 chained tool calls per turn bounds latency + LLM cost.
- maxDuration raised 30s → 60s to absorb tool-chain runs.
- Result: one user prompt now triggers, e.g. search_installations →
energy_savings_calculator → show_case_study → schedule_consultation
in a single turn — exactly the SPIN methodology in the prompt.
RATE LIMITING (src/lib/rateLimit.ts + api/chat)
- Token-bucket per IP: 30 messages burst, sustained 30/minute. Trips
to 429 with Retry-After + X-RateLimit-Remaining headers when abused.
- IP extracted from x-forwarded-for (Nginx already passes this).
- In-memory Map with 10-min GC of stale buckets — no Redis dep.
If we scale to multiple replicas later, swap the Map for Upstash.
- Protects the OpenAI quota from someone hammering the chat endpoint.
IMAGE PIPELINE (src/lib/imageOptimizer.ts)
- sharp-based optimizer: auto-orient (EXIF), cap at 2560px long side,
re-encode WebP@85, content-hash filename. Re-uploads with same
content reuse the same hash; new content gets a new URL — perfect
cache invalidation without header tricks.
- Opt-in via optimize=1 form/query param on /api/assets POST.
- Hero CMS and Site Settings uploads turn it on automatically (those
are user-facing brand assets where compression matters most).
- App/news/parts uploads remain untouched (editors may be uploading
CAD drawings, datasheets, etc. that shouldn't be transcoded).
- Falls back gracefully to a no-op for unsupported formats (SVG, GIF,
videos, anything sharp can't decode) so it never breaks an upload.
DOCKERFILE
- Adds vips/vips-dev for sharp on Alpine + --include=optional so the
@img/sharp-linuxmusl-x64 prebuilt is downloaded
- Explicitly copies node_modules/sharp + node_modules/@img to the
runner stage (Next.js trace can miss conditional deps).
NO DB SCHEMA CHANGES.