Forge Phase 2 shipped. PilotLight got a safety gate and 58 tests. The ForgeKit website went from nothing to fully deployed — 7 pages, community hub, real screenshots, live flame. One day.
drizzle-kit push failed with url: undefined. Root cause: vercel env pull only downloads Vercel system vars (VERCEL_ENV, TURBO_CACHE) — not DATABASE_URL or any app secrets.
Manually add DATABASE_URL to .env.local. Never rely on vercel env pull for secrets.
put(..., { access: 'public' }) threw "Cannot use public access on a private store." Private stores require private access on every upload.
Changed to access: 'private' + built server-side proxy route. Bonus: photos now org-scoped.
Passing a private Vercel Blob URL to Claude's source: { type: 'url' } returns 400 — Claude's API can't download authenticated URLs.
Fetch bytes server-side with the blob token, encode as base64, send as source: { type: 'base64', media_type, data }.
Caption generation failed with model: claude-sonnet-4-6 — a byte-order mark prepended the model string. Caused by pasting into Vercel's env var field instead of typing.
Delete the var and manually retype the value. Never paste model IDs into Vercel.
Code checked !callStatus to detect the initial call webhook. Twilio sends CallStatus: ringing on the initial POST — not an empty string.
Check for callStatus === 'ringing' || !callStatus explicitly.
Returning { ok: true } caused Twilio error 12300 "Invalid Content-Type." Voice webhook must return XML.
Return new NextResponse(twiml, { headers: { 'content-type': 'text/xml' } }).
"accountSid must start with AC" — env var was in .env.local but never added to Vercel dashboard (same vercel env pull problem).
Manually add all Twilio vars to Vercel dashboard. Never assume pull gets them.
SMS code confirmed working in Vercel logs. Carrier silently drops the message. New EINs take 2–4 weeks to propagate through carrier verification. Sole proprietor brand submitted — pending.
No code fix. Block: wait for A2P approval. Plan for this lead time before every demo.
Twilio Account SID, Auth Token, and API SID appeared in chat during debugging. Clerk and Anthropic keys visible in a .env.local screenshot.
Auth Token rotated immediately. All shared creds should be rotated before Gino goes live.
When Twilio's own debugger only showed an error code, Vercel logs showed the actual values (toNumber, callSid, callStatus, "SMS sent successfully"). Faster and more specific than any other path.
Fetch private bytes server-side, encode to base64, send as source: { type: 'base64' }. Works for any private file store. Extracted to lib/ai.ts — reusable across all ForgeKit apps.
Storing blob pathnames in DB + serving through an authenticated API route means photos are fully org-scoped. No signed URLs, no expiring links, no token exposure to the browser.
TWILIO_API_KEY + TWILIO_API_SECRET is the right production pattern. Revocable per-project without affecting the whole Twilio account.
Added flame above the headline in a single-column layout — it shoved everything down and made the hero too tall.
Moved to a flex row: text left (flex-1), flame right (hidden md:flex shrink-0). Hidden on mobile.
Every background image needed 2–3 opacity iterations. Pattern: low opacity + dark gradient layer compound into near-invisible. Started at 7–20% and had to rebuild up each time.
Rule going forward: start at 50%+ opacity and pull back. Don't build up from dark.
Screenshot showed "too dark" — edited the hero section. The actual dark section was the CTA further down the page.
Identify the section name in code before touching opacity. Read the screenshot context.
Iterating one image opacity at a time burned through the free tier daily limit. About page fix and PWA manifest are committed but couldn't be deployed.
Batch image iterations into one deploy. The limit resets at midnight UTC.
Pull quote had a standalone <p>"</p> element above the quote text, rendering as a lone hanging quotation mark on its own line.
Removed the element. Embedded opening quote directly into the quote text string.
Plat map from Flyback and RockGod from FretHero gave the community pages instant character. Zero sourcing cost. Tells the story of what's already built — the website sells the apps by showing the work.
The PilotLight flame from the app is a pure SVG with CSS keyframe animation. Inlined directly in a Next.js server component with a <style> tag — no "use client", no extra component, identical to the app. One source of truth, two places.
WebSearch → WebFetch the photo page for the images.unsplash.com URL → PowerShell Invoke-WebRequest to download. Fully automated, free license, no manual steps. This is the pattern for every future image need.
Drove the live Forge app with Playwright to capture actual product screens. The Forge page shows real software, not mockups. Karen Stills job data makes it look like a real contractor account.
/smoke\s*(coming|pouring|filling)/i didn't match "smoke is filling" — "is" sits between "smoke" and "filling". The test suite caught it immediately before it could ever reach production.
// Before — didn't match "smoke is filling" /smoke\s*(coming|pouring|filling)/i // After — covers both forms /smoke\s*(coming|pouring|filling|is\s+filling)/i
Extended the pattern. This is exactly what the test suite exists for.
Tried to mock softStopCheck inside describe blocks. Vitest hoists vi.mock at compile time — mocks inside describe blocks don't work as expected. Same issue hit in Forge v2 tests.
Replaced mocked softStopCheck tests with behavior contract tests: no API key → false, empty input → false, error → false. Tests are more meaningful and don't fight Vitest mechanics.
Hard regex stops the obvious cases (18 patterns, zero API cost). Claude handles the edges. Any error in either tier returns false — a network hiccup never blocks a real user. The safety gate is trustworthy precisely because it fails safe.
The "smoke is filling" miss was found by Vitest immediately — before deployment. This is the whole point. 18 safety patterns need 18 test cases, not assumptions. Tests aren't bureaucracy here; they're the thing that makes the safety gate credible.
Instead of fighting Vitest's hoisting to mock softStopCheck, wrote tests for its contracts — what it should return given specific inputs and conditions. More robust, more meaningful, no mock mechanics to maintain.
| App / Phase | What it covers | Status |
|---|---|---|
| Forge — Phase 0 | Pipeline, contacts, auth, DB, demo data | ✓ Done |
| Forge — Phase 1 | Twilio, Inngest sequences, Automation Hub | ✓ Done |
| Forge — Phase 2 | Photos, AI captions, dashboard stats | ✓ Done |
| Forge — A2P 10DLC | Carrier SMS delivery for Gino's number | ⚠ Pending (EIN verification, 2–4 wks) |
| Forge — Phase 3 | Document Q&A, pgvector RAG, second vertical | Not started |
| PilotLight — Core | Guide flow, camera, schema, rate limiting | ✓ Done |
| PilotLight — Trust | Safety gate (2-tier), thumbs feedback, Vitest | ✓ Done today |
| ForgeKit Website | 7 pages, community hub, email, PWA icon | ✓ Live at forgekits.build |
| Website — About page opacity | Committed, pending Vercel deploy limit reset | ⚠ Pending deploy |
Only pulls Vercel system vars. Always manually populate DATABASE_URL and all app secrets in .env.local. Never rely on pull for secrets.
Pasting from clipboard introduces invisible BOM characters. Retype claude-sonnet-4-6 manually in Vercel env var fields every time.
Claude's API can't fetch authenticated URLs. Fetch bytes server-side, encode to base64, send as source: { type: 'base64' }.
New EINs take 2–4 weeks to propagate to carrier databases. Submit registration immediately when any client needs SMS. Never schedule a demo around unregistered SMS.
Treat it as compromised. Rotate before going live with any client, regardless of how the chat was handled.
Building up from low opacity requires too many iterations. Dark overlay + low base opacity compound — the image disappears. Start high, reduce to taste in one or two passes.
When a screenshot shows "too dark," find the section name in the code before touching anything. Don't assume it's the hero.
Before sourcing stock images, check if existing apps already have on-brand visuals. Flyback has maps; FretHero has RockGod. Zero cost, tells a richer story.
Free Vercel tier is 100 deploys/day. Don't deploy after every opacity tweak — batch into one deploy when the direction is clear.
vi.mock doesn't work inside describe blocks. For AI utility functions, write behavior contract tests (no key → false, error → false) instead of fighting mock hoisting.
"If I can think it clearly enough, I can build it."