Blog

Integration3 min read

Next.js Plus fal: A Pattern That Scales

Server actions, edge routes, client-side submissions. The split that keeps your bundle small and your API key safe.


The split that keeps your bundle small and your key safe

Code runs in three places in a modern Next.js app: browser, edge, Node server. Your fal key is allowed in exactly one: the Node server. That means route handlers on the Node runtime, or server actions.

Edge works for many calls, but its fetch surface and streaming semantics are a separate profile. Pick Node for fal calls unless you have a strong reason not to.

The shape

Browser to edge to fal control flow
Browser to edge to fal control flow
  1. Browser: a form, a preview, a gallery. No fal import. No key.
  2. Server action or route handler: reads the key from env, calls fal, persists the request id.
  3. fal.ai: runs the model.

The browser never imports @fal-ai/client. Treat the package as server only.

A server action for short jobs

For anything under three minutes with a spinner, subscribe inside a server action is the shortest path.

TYPESCRIPT
1// app/actions/render.ts
2"use server";
3
4import { fal } from "@fal-ai/client";
5import { cookies } from "next/headers";
6
7fal.config({ credentials: process.env.FAL_KEY });
8
9export async function renderShort(prompt: string) {
10 const userId = (await cookies()).get("uid")?.value ?? "anon";
11
12 const result = await fal.subscribe("fal-ai/pixverse/v6/text-to-video", {
13 input: { prompt, duration: 5, resolution: "1080p" },
14 logs: true,
15 });
16
17 await db.insert("generations", {
18 user_id: userId,
19 request_id: result.requestId,
20 endpoint: "fal-ai/pixverse/v6/text-to-video",
21 cost_cents: Math.round(5 * 3), // Pixverse v6 is $0.03-$0.12/sec (tiered); 5s at 360p no audio = $0.15
22 video_url: result.data.video.url,
23 });
24
25 return { url: result.data.video.url, requestId: result.requestId };
26}

Pixverse v6 at $0.03-$0.12/sec (tiered) is the right model for iterative UIs. Users click many buttons.

A route handler for long jobs

For anything over three minutes (Veo 3.1 at 4K, Kling v3 Pro multi prompt), do not hold the HTTP connection. Submit, return the request id, let a webhook finish.

TYPESCRIPT
1// app/api/render/route.ts
2import { fal } from "@fal-ai/client";
3
4export const runtime = "nodejs";
5
6export async function POST(req: Request) {
7 fal.config({ credentials: process.env.FAL_KEY });
8 const { prompt } = await req.json();
9
10 const { request_id } = await fal.queue.submit("fal-ai/veo3.1", {
11 input: { prompt, duration: "8s", resolution: "4k", aspect_ratio: "16:9" },
12 webhookUrl: `${process.env.APP_URL}/api/fal/webhook`,
13 });
14
15 await db.insert("generations", { request_id, status: "IN_QUEUE" });
16 return Response.json({ requestId: request_id });
17}

An 8 second Veo 3.1 4K clip is $3.20. You do not want that on a serverless function that might time out.

The button, client side

Server action wired to fal with locked key
Server action wired to fal with locked key
TSX
1"use client";
2
3import { useTransition, useState } from "react";
4import { renderShort } from "./actions/render";
5
6export default function Page() {
7 const [isPending, start] = useTransition();
8 const [url, setUrl] = useState<string | null>(null);
9
10 return (
11 <form action={(fd) => start(async () => {
12 const r = await renderShort(String(fd.get("prompt")));
13 setUrl(r.url);
14 })}>
15 <input name="prompt" />
16 <button disabled={isPending}>{isPending ? "rendering..." : "go"}</button>
17 {url && <video src={url} controls />}
18 </form>
19 );
20}

No key, no fal import, no endpoint string in the bundle.

Edge runtime: only if you have a reason

Edge gives you lower cold start. It also constrains you: stricter fetch, different timeout profile. For fal.queue.submit (one POST) edge is fine. For fal.subscribe on a long job, edge will time out earlier than Node. Default to Node, flip to edge only for short POSTs.

Environment and secrets

BASH
1FAL_KEY=sk-...
2APP_URL=https://your-app.vercel.app
3FAL_WEBHOOK_SECRET=whsec_...

Vercel's env UI, never commit. Add to production and preview. Skip development unless you actually call real endpoints from local.

What not to do

Do not import @fal-ai/client in a client component. Do not pass the key to the browser. Do not run subscribe on a function with a 10 second max duration. Do not skip writing the request id at submit time.

Get this split right once and the rest of the app stops having bundle and secrets questions.