Generate YouTube thumbnails and blog cover images from a prompt, an image you already have, or a blog URL.
| Route | What it does |
|---|---|
/studio/text-to-image |
Describe the thumbnail; the app writes a design prompt and generates it. |
/studio/image-to-image |
Upload photos (a face, a product) and composite them into a thumbnail. |
/studio/blog-cover |
Paste a blog URL. The page is fetched and read, and a cover image is designed from its actual content. |
Every workflow outputs YouTube (16:9, 1280×720) and Shorts (9:16, 720×1280).
- Next.js 15 (App Router) and React 19
- Clerk for authentication
- Prisma and PostgreSQL
- Fal AI for image generation, OpenAI for prompt authoring
- Razorpay for payments
- Tailwind CSS v4 with shadcn/ui
Defined in one place, src/config/models.ts. Swapping a model is a change
there rather than in the route handlers.
| Tier | Endpoint | Cost | Credits |
|---|---|---|---|
| Draft | openai/gpt-image-2 (medium) |
~$0.040 / image | 1 |
| Quality | fal-ai/nano-banana-pro (2K) |
~$0.150 / image | 3 |
| Edit | fal-ai/nano-banana-2/edit |
~$0.080 / image | 2 |
Prompt authoring runs on PROMPT_MODEL in the same file, overridable with
OPENAI_PROMPT_MODEL.
git clone <repo> && cd thumbai
npm install # also runs prisma generate
cp .env.example .env # then fill it in, see below
npm run db:up # local Postgres via docker compose
npm run db:deploy # apply migrations to it
npm run devOpen http://localhost:3000.
Production runs on Neon. Local runs on a Postgres container, so next dev
cannot reach production data.
| File | Holds | Loaded |
|---|---|---|
.env |
local DATABASE_URL plus all other keys |
automatically by Next |
.env.prod |
only the production DATABASE_URL |
never automatically |
.env.prod is not a Next.js convention, which is the point: nothing picks it
up by accident. Production commands opt in explicitly and print the target
host before acting.
| Command | Target |
|---|---|
npm run db:up / db:down |
local container |
npm run db:migrate |
local — create and apply a new migration |
npm run db:deploy |
local — apply existing migrations |
npm run db:studio |
local — browse data |
npm run db:status:prod |
production — show migration state |
npm run db:deploy:prod |
production — apply migrations |
npm run db:backup:prod |
production — pg_dump into backups/ |
npm run build runs prisma migrate deploy first, so a deploy cannot ship
code ahead of the schema it needs. That ordering has bitten twice: once when a
String[] to enum change went out before the code that understood it, and once
when a new column was added in code but never migrated. The build step is the
only pre-deploy hook Vercel offers, so it is where this belongs.
Back up before migrating production: npm run db:backup:prod. Dumps land in
backups/, which is gitignored because they contain real user data.
node --env-file=.env tools/grant-admin.mjs # list users (read-only)
node --env-file=.env tools/grant-admin.mjs you@example.com 100 # promote + set creditsEmail is not unique in the schema, so the script refuses to act when more than one row matches and prints the ids instead.
Copy .env.example and fill in every value — the app validates them at startup
via src/config/env.ts and will refuse to boot if any are missing.
There is deliberately no NEXT_PUBLIC_FAL_KEY. Anything prefixed
NEXT_PUBLIC_ is inlined into the browser bundle, so a Fal credential there is
readable by any visitor and spendable against your account. Uploads go through
/api/upload, which holds the key server side.
Two endpoints need to be reachable from the internet, so use a tunnel
(ngrok http 3000) in development:
| Provider | Endpoint | Purpose |
|---|---|---|
| Clerk | /api/webhook/register |
Creates the local User row on sign-up. Without it, sign-in succeeds but every API call returns 401. |
| Razorpay | /api/webhook/payment |
Grants credits after a verified payment. |
| Fal | /api/fal/webhook |
Receives finished generations. Set NEXT_PUBLIC_FAL_WEBHOOK_URL to the tunnel origin. |
Fal deliveries are verified against Fal's ED25519 JWKS, so they must arrive with their original signature headers intact.
| Command | Purpose |
|---|---|
npm run dev |
Development server |
npm run build |
Production build |
npm run start |
Serve the production build |
npm run typecheck |
tsc --noEmit |
npm run format |
Prettier |
Credits are server-authoritative. They are deducted inside the generation
routes with a conditional update guarded on credits >= cost, so concurrent
requests cannot overdraw, and refunded if the provider call fails. Nothing on
the client can move a balance.
Generation is queued, not blocking. Both /api/generate and /api/edit
submit to Fal's queue and return a request_id. The browser subscribes to
/api/result-stream (SSE, owner-only, capped at 5 minutes) while Fal calls
/api/fal/webhook with the result.
Plan pricing lives in src/config/plans.ts. Checkout sends only a plan id;
the price charged and the credits granted are both resolved server side.
MIT