I’ve been building Image Banana — a web-based AI image editor where you can upload a photo and describe edits in plain English (for example: “replace the background with a snowy mountain”, “keep the same character, change the outfit”, “make it golden hour”).
site:https://www.my-nano-banana.com/
This post walks through the architecture and a few implementation details that made the product feel “real” quickly:
- Image-to-image editing and text-to-image generation
- One-shot editing (one prompt -> final output)
- Batch processing (multiple inputs) as a premium feature
- A guest trial: 1 generation without signing up, then prompt Google sign-in
- A simple credits + subscription model
If you want to try it live, replace the placeholders below with your domain and deploy it.
Product Decisions That Shaped the Code
Before writing code, I forced myself to answer:
1) What does the “happy path” look like in under 30 seconds?
2) What’s the minimum gating needed to prevent abuse, without killing the first-run experience?
3) Which features should be paid (and why)?
That led to:
- Guest trial: let people generate once without login.
- Batch mode: premium because it’s costlier and easier to abuse.
- Clear error UX: show “why” (credits, subscription required, missing inputs) rather than “something went wrong”.
High-Level Architecture
The app is a standard Next.js App Router stack:
- UI:
components/image-editor.tsx(client component) - Generation API:
app/api/generate/route.ts(Node runtime) - Auth:
- Start OAuth:
app/auth/login/route.ts - Callback:
app/auth/callback/route.ts
- Start OAuth:
- Billing primitives:
lib/billing/*(supports Supabase-backed billing, with a local JSON fallback for dev)
The core request flow:
1) Browser collects prompt + optional uploaded image(s)
2) POST to /api/generate
3) Server calls OpenRouter (Gemini image models) and extracts returned image URLs
4) UI renders returned image(s) and offers download / “edit again”
Image Generation via OpenRouter (Gemini)
At a high level, the server makes a chat/completions request with modalities ["image","text"].
Implementation notes:
- Timeouts matter (don’t let requests hang forever).
- Be strict with payload validation: missing prompt, missing reference image in image-to-image mode, etc.
- When the model returns images, you typically get hosted URLs (not base64). Your UI needs to treat them as remote URLs.
Minimal (simplified) request shape:
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
"Content-Type": "application/json",
"HTTP-Referer": process.env.OPENROUTER_HTTP_REFERER ?? "http://localhost:3000",
"X-Title": process.env.OPENROUTER_X_TITLE ?? "image-banana",
},
body: JSON.stringify({
model: "google/gemini-2.5-flash-image",
modalities: ["image", "text"],
messages: [
{
role: "user",
content: [
{ type: "text", text: prompt },
...(imageUrl ? [{ type: "image_url", image_url: { url: imageUrl } }] : []),
],
},
],
}),
})
Guest Trial: “One Free Generation” Without Login
The UX goal: let users try the product immediately.
The abuse-prevention goal: don’t give unlimited free generations.
I implemented guest trial as:
- Client-side: a localStorage flag for fast UI gating (
image-banana-guest-trial-used) - Server-side: an HTTP cookie (
ib_guest_trial_generate_v1) so the API is the source of truth
This way, even if someone clears localStorage, the API still enforces the “one free generation” rule.
On the server (/api/generate), the logic looks like:
- If not logged in:
- if batch image-to-image ->
401(auth required) - if trial cookie already set ->
401(trial exhausted) - else allow exactly one request, then set the trial cookie
- if batch image-to-image ->
And the client reacts to 401 by opening a sign-in dialog or redirecting to /auth/login.
Batch Processing as a Premium Feature
Batch mode is great UX, but it’s also a cost multiplier.
I made it premium by checking entitlements:
- Batch requires an active subscription
- Single requests consume credits
- In a batch, if some items fail, refund the credits for failed items (so you only pay for successes)
This “refund failed items” detail is small, but it prevents a lot of frustration.
Google Sign-In with Supabase Auth (Avoiding Open Redirects)
OAuth flows love to break in subtle ways. One place I’m careful: the next parameter.
If you accept an arbitrary next URL, attackers can create “open redirect” links.
So the auth routes sanitize next:
- must start with
/ - must not start with
// - if invalid -> fall back to
/
Then the login route starts the Supabase Google flow and redirects back to:
/auth/callback?next=<safe_next>
Billing: Credits + Subscriptions (with a Local Dev Fallback)
I wanted billing logic I could run locally without setting up every external dependency immediately.
So the project supports:
-
Supabase-backed billing when
SUPABASE_SERVICE_ROLE_KEYis present -
Local billing otherwise (
.local/billing.json)
That gives you a nice dev experience:
- run the UI
- test credits/subscriptions
- only wire webhooks/payments when you’re ready
SEO Cold Start Checklist (So You Don’t Launch Invisible)
If you’re shipping a product site, SEO isn’t “later” — it’s launch-critical. I added:
-
robots.txtwith sensible disallows and a sitemap pointer - a single sitemap source (
/sitemap.xml) - page-specific title/description on key pages
-
hreflangalternates for locales - default
noindexfor mirrored locales (until translations are real)
Even a basic setup helps you avoid the most common cold-start traps.
What I’d Improve Next
- Real translations for
/zhand/ko(or remove them until ready) - Better prompt presets per use case (product shots, portraits, UGC, etc.)
- A job queue for long-running generations + webhooks
- Real background removal (current clone has a mocked tool page)
If You Want to Build Your Own Version
If you want to replicate the core stack, you’ll need:
- Node.js
>= 20.9 - OpenRouter API key
- Supabase project + Google OAuth enabled
- A payment provider (this project uses Creem)
And then start from:
-
/generator(the main product surface) -
/api/generate(the core API)
If you’re building something similar, I’d love to hear:
- How you gate free usage without ruining first-run UX
- Whether you prefer “credits” vs “subscription-only” for image tools
- What you consider “must-have” editing features beyond background replacement
Top comments (0)