← All projects
Flagship · Personal · Solo build · 2026-Q1 → present · … min read

Reelforge — Multi-model AI Video & Image Generation Platform

Agencies already pay Runway for hero shots, Kling for volume, Pika for social. Reelforge is the one-wallet router that lets them swap models per-mode without switching tools — across fal.ai's full catalog of 17 endpoints, with a Director Mode that goes script → storyboard → parallel renders → stitched MP4.

Next.js 15 React 19 TypeScript strict fal.ai Neon Postgres + Drizzle Cloudflare R2 Clerk OpenAI gpt-4.1-mini ffmpeg Bun

Product thesis

The video-AI market is exploding into specialized models. Kling 2.5 is great at human motion, Seedance is great at cinematic motion control, Wan 2.5 is fast and cheap, Veo is the prestige play, Nano Banana is for fast image edits. Agencies and creators today juggle subscriptions, dashboards, and credits across half a dozen vendors.

Reelforge collapses that into one router with one wallet, one prompt enhancer, and one consistent UI. The differentiator: a Director Mode that takes a script, plans a storyboard, fires shots in parallel across whatever models suit each shot, and stitches the result into a single MP4 — server-side, with ffmpeg.

Architecture highlights

Discriminated-union model registry

One GenerateInput TypeScript union covers 17 fal endpoints across 6 generation modes (T2V, I2V, F2F, MC, MR, T2I, I2I). Zod-validated at the API boundary; the dashboard form, the API route, and buildFalInput() all narrow against the same type. Per-model invariants live in two helpers — clampDuration(model, n) and clampAspect(model, s) — that encode the fal endpoint quirks I learned the hard way (e.g. wan-2.5 only accepts duration ∈ {5, 10}; kling-2.5-i2v inherits aspect from the input image and rejects an explicit one). Six concrete invariants are codified as non-revertible rules in AGENTS.md so the next person doesn't relearn them.

Async generation lifecycle

POST /api/generate writes a row, hands the request to fal.queue.submit(), and returns instantly. Two paths follow:

  • Polling via /api/generations/[id] with exponential backoff — cheap, works everywhere, used by the dashboard.
  • Webhook receiver at /api/webhooks/fal — the production path with APP_URL, fires when fal calls back, updates the row.

One retry on retryable 4xx/5xx; validation errors fail fast. enrichFalError() unwraps fal's generic "Unprocessable Entity" into field-level ApiError.body.detail reasons so users see "aspect_ratio: must be one of 16:9, 9:16, 1:1" instead of a useless HTTP status.

Director Mode v2

A five-phase resumable workspace (Script · Lookbook · Shotlist · Sequence · Export) persisted across 6 Postgres tables. GPT-4.1-mini is the storyboard planner; lookbook-first character/location consistency means the planner's shots reference established lookbook assets instead of redrawing the world each shot. Shots render in parallel; server-side ffmpeg stitches the result into a single MP4. v1 is preserved at /director/v1 for comparison.

Cost transparency

Per-model estimateCost() drives a single calculator that flows into the dashboard preview, per-shot cost line, and project-cost rollup. Regression-tested at ±4% (a 5s MR shot ≈ $1.51). The function is opinionated about the gap between published and real rates — fal's published $0.18/s for seedance-2-ref is real for video refs but jumps to $0.3024/s for image refs. I learned this by burning $2.90 on two demo renders before realizing the discount didn't apply.

Hard problems solved

17
fal endpoints under one typed union
6
generation modes routed through one dashboard
~18.7k
LOC of TS/TSX, 130 files, solo
±4%
Cost-estimator regression tolerance

1. fal.ai schema drift

Every video endpoint claims to take duration: number, but several reject it as a string-typed enum subset. I wrote clampDuration / clampAspect after a wan-2.5 422 storm and codified them as invariants. Six concrete invariants live in AGENTS.md.

2. Mock-vs-prod cost divergence

fal published $0.18/s for seedance-2-ref. Real billing came in at $0.3024/s for image refs. Caught after burning $2.90 on two demo renders. Now regression-tested.

3. WSL2 + Neon serverless drivers

Neon's websocket and HTTP serverless drivers both fail on WSL2 because Node's fetch + happy-eyeballs interleaves IPv6/IPv4 lookups in a way Neon's WS handshake can't survive. Switched to pg over plain TCP with forced IPv4 DNS order at module load in lib/db/index.ts. Documented as a non-revertible invariant.

4. Generic error masking

fal's queue surfaces 422s as "Unprocessable Entity" with the real reason buried inside ApiError.body.detail. Wrote enrichFalError() to unwrap the ValidationError shape so the user sees the actual field that broke instead of a useless status code.

5. Director Mode planner blind spot

GPT proposed I2V shots without references because it didn't know what was rendered yet. resolveShotReferences() auto-binds the first lookbook location URL or falls back to T2V; the planner can no longer deadlock the renderer.

Skills exercised

  • Production LLM apps — prompt engineering across 11 verified models, structured-output planners (storyboard generation), Zod-validated tool boundaries.
  • Discriminated unions at the system boundary — one type for 6 modes × 11 models, narrowed in API routes, in buildFalInput, in the dashboard form.
  • Async job orchestration — queue submit + polling + webhooks + retries + error normalization.
  • Postgres schema design — Drizzle as source of truth, manual psql migrations, indexes added when count(*) rate-limit checks slowed down.
  • Edge storage — R2 presigned uploads, server-side mirroring of generation outputs, cost-aware bandwidth.
  • Design systems from first principles — Dark Room Editorial token system; warm paper↔ink with an ember #ea7a3c accent; Simple/Pro dual-mode contract; AI affordance language (✦ glyph + dashed ember border).
The thing I'm most proud of isn't the model count — it's that the type system catches a wan-2.5 duration mismatch at compile time, the API surface catches it at the boundary, and the dashboard never lets the user submit it. Three layers, one source of truth.

Stack

Next.js 15 · React 19 · TypeScript strict · Clerk · Neon Postgres · Drizzle · pg · Cloudflare R2 · fal.ai · OpenAI gpt-4.1-mini · ffmpeg · Tailwind 3 · Radix UI · Bun test