Nano Banana
Generate stunning AI images directly in your Convex backend with persistent storage, real-time status tracking, and a clean TypeScript API.
git clone https://github.com/dperussina/convex-nano-banana.git
cd convex-nano-bananaThis upstream package has not been adapted or qualified for Bijection. Read the original documentation before adopting it.
Description
Generate stunning AI images directly in your Convex backend with persistent storage, real-time status tracking, and a clean TypeScript API.
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: dperussina/convex-nano-banana. License: See repository. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
AI Image Generation Component for Convex powered by Gemini
Upstream overview
From the upstream package documentation.
Nano Banana integrates Google Gemini's AI image generation directly into your Convex backend with persistent file storage and real-time status tracking. It provides a TypeScript API for text-to-image generation and image editing, handling the complete lifecycle from generation through storage in Convex. The component includes reactive queries that automatically update as generation progresses, plus user management and generation history tracking.
Use cases
From the upstream package documentation.
- Content creation apps - Build social media platforms or creative tools where users generate custom images from text prompts with real-time progress updates
- E-commerce product visualization - Generate product mockups or lifestyle images by editing existing product photos with text instructions
- Multi-tenant SaaS applications - Implement AI image generation features where each customer uses their own Gemini API key for billing isolation
- Content moderation workflows - Generate test images for training moderation systems or create placeholder content with specific characteristics
- Creative collaboration platforms - Allow users to iterate on images through text-based editing while maintaining version history and persistent storage
How it works
From the upstream package documentation.
The component wraps Google Gemini's image generation API in a Convex-native interface, automatically handling file storage and status tracking. You initialize the NanoBanana class with your component reference and optional default configuration, then call methods like generate() for text-to-image or edit() for image modification.
Generation requests create database records with status tracking (pending, generating, complete, failed) that update reactively through Convex queries. Images are automatically stored in Convex file storage and linked to generation records. The get() method provides real-time status updates that trigger React re-renders as generation progresses.
The component supports multiple Gemini models and accepts configuration options like aspect ratios and image sizes. API keys can be set globally during initialization or passed per-request for multi-tenant scenarios. All generations are tied to user IDs for access control and include timing metrics and error handling.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Nano Banana with Bijection.
Read: https://bijection.com/components/convex-nano-banana/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from dperussina/convex-nano-banana, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
🍌 Nano Banana
AI Image Generation Component for Convex, powered by Google Gemini
Generate stunning AI images directly in your Convex backend with persistent storage, real-time status tracking, and a clean TypeScript API. Bring your own Gemini API key.
✨ Features
- 🎨 Text-to-Image Generation - Create images from text prompts using Gemini's native image generation
- ✏️ Image Editing - Edit existing images with text instructions
- 📦 Persistent Storage - Images automatically stored in Convex file storage
- ⚡ Real-time Status - Reactive queries update as generation progresses
- 🔮 Future-proof - Accepts any model name for upcoming Gemini models
- 🔐 BYO API Key - Users provide their own Gemini API key (secure, server-side only)
- 📊 Generation History - Track all generations with timing metrics
📦 Installation
npm install convex-nano-banana
🚀 Quick Start
1. Add the component to your Convex app
Create or update convex/convex.config.ts:
import { defineApp } from "convex/server";
import nanoBanana from "convex-nano-banana/convex.config";
const app = defineApp();
app.use(nanoBanana);
export default app;
2. Create your image generation functions
Create convex/images.ts:
import { v } from "convex/values";
import { action, query, mutation } from "./_generated/server";
import { components } from "./_generated/api";
import { NanoBanana } from "convex-nano-banana";
// Initialize the client
const nanoBanana = new NanoBanana(components.nanoBanana, {
// Optional: Set a default API key (can also be passed per-request)
// GEMINI_API_KEY: process.env.GEMINI_API_KEY,
});
// Generate an image
export const generateImage = action({
args: {
prompt: v.string(),
apiKey: v.string(),
aspectRatio: v.optional(v.string()),
},
handler: async (ctx, args) => {
return await nanoBanana.generate(ctx, {
userId: "user_123", // Replace with actual user ID from auth
prompt: args.prompt,
aspectRatio: args.aspectRatio,
GEMINI_API_KEY: args.apiKey,
});
},
});
// Get generation status (reactive!)
export const getGeneration = query({
args: { generationId: v.string() },
handler: async (ctx, args) => {
return await nanoBanana.get(ctx, args);
},
});
// List user's generations
export const listGenerations = query({
args: { userId: v.string() },
handler: async (ctx, args) => {
return await nanoBanana.list(ctx, args);
},
});
// Delete a generation
export const deleteGeneration = mutation({
args: { generationId: v.string() },
handler: async (ctx, args) => {
return await nanoBanana.delete(ctx, args);
},
});
3. Use in your React app
import { useAction, useQuery } from "convex/react";
import { api } from "../convex/_generated/api";
import { useState } from "react";
function ImageGenerator() {
const [prompt, setPrompt] = useState("");
const [generationId, setGenerationId] = useState<string | null>(null);
const generate = useAction(api.images.generateImage);
const generation = useQuery(
api.images.getGeneration,
generationId ? { generationId } : "skip"
);
const handleGenerate = async () => {
const id = await generate({
prompt,
apiKey: "YOUR_GEMINI_API_KEY", // In production, handle securely!
});
setGenerationId(id);
};
return (
<div>
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe your image..."
/>
<button onClick={handleGenerate} disabled={!prompt}>
Generate
</button>
{generation?.status === "generating" && (
<p>🎨 Generating your image...</p>
)}
{generation?.status === "complete" && generation.imageUrl && (
)}
{generation?.status === "failed" && (
<p>❌ Error: {generation.error}</p>
)}
</div>
);
}
📚 API Reference
NanoBanana Class
import { NanoBanana } from "convex-nano-banana";
const nanoBanana = new NanoBanana(components.nanoBanana, {
GEMINI_API_KEY: "optional-default-key",
defaultModel: "gemini-2.5-flash-image", // Optional
});
Methods
generate(ctx, options) → Promise<string>
Generate a new image from a text prompt.
const generationId = await nanoBanana.generate(ctx, {
userId: "user_123", // Required: User identifier
prompt: "A futuristic city", // Required: Image description
model: "gemini-2.5-flash-image", // Optional: Model to use
aspectRatio: "16:9", // Optional: Aspect ratio
imageSize: "2K", // Optional: For Pro models
GEMINI_API_KEY: "...", // Optional: Override API key
});
edit(ctx, options) → Promise<string>
Edit images using a text prompt and input images.
const generationId = await nanoBanana.edit(ctx, {
userId: "user_123",
prompt: "Add a rainbow to the sky",
inputImages: [{
base64: "...",
mimeType: "image/png",
}],
GEMINI_API_KEY: "...",
});
editFromStorage(ctx, options) → Promise<string>
Edit using previously stored images (by storage ID).
const generationId = await nanoBanana.editFromStorage(ctx, {
userId: "user_123",
prompt: "Make it more colorful",
inputStorageIds: ["storage_id_here"],
GEMINI_API_KEY: "...",
});
get(ctx, { generationId }) → Promise<Generation | null>
Get a generation by ID. This is a reactive query - in React, it automatically updates when the status changes.
const generation = await nanoBanana.get(ctx, {
generationId: "abc123",
});
// Returns: { status, imageUrl, prompt, durationMs, ... }
list(ctx, { userId, limit?, cursor? }) → Promise<ListResult>
List generations for a user with pagination.
const { generations, hasMore, cursor } = await nanoBanana.list(ctx, {
userId: "user_123",
limit: 20,
});
listByStatus(ctx, { status, limit? }) → Promise<Generation[]>
List generations by status.
const failed = await nanoBanana.listByStatus(ctx, {
status: "failed",
limit: 10,
});
delete(ctx, { generationId }) → Promise<void>
Delete a generation and its stored images.
await nanoBanana.delete(ctx, { generationId: "abc123" });
deleteAllForUser(ctx, { userId }) → Promise<{ deleted: number }>
Delete all generations for a user.
const { deleted } = await nanoBanana.deleteAllForUser(ctx, {
userId: "user_123",
});
🎯 Supported Models
| Model | Description | Resolution |
|---|---|---|
gemini-2.5-flash-image | Fast generation (default) | 1024px |
gemini-3-pro-image-preview | High quality, up to 4K | 4096px |
Future-proof: Pass any model name string - when Google releases new models, they'll work immediately!
📐 Aspect Ratios
Supported values: "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"
📏 Image Sizes (Pro Model Only)
For gemini-3-pro-image-preview: "1K", "2K", "4K"
📊 Generation Status
| Status | Description |
|---|---|
pending | Created but not started |
generating | Currently generating |
complete | Successfully generated |
failed | Generation failed (check error field) |
🔐 Security Best Practices
Never expose your Gemini API key to clients!
Option 1: Environment Variable (Recommended)
# Set in Convex dashboard or .env.local
npx convex env set GEMINI_API_KEY your-key-here
// In your action
const nanoBanana = new NanoBanana(components.nanoBanana, {
GEMINI_API_KEY: process.env.GEMINI_API_KEY,
});
Option 2: Per-User Keys (Multi-tenant)
Store keys securely and pass them server-side:
export const generateImage = action({
args: { prompt: v.string() },
handler: async (ctx, args) => {
const user = await getAuthenticatedUser(ctx);
const apiKey = await getUserApiKey(ctx, user.id);
return await nanoBanana.generate(ctx, {
userId: user.id,
prompt: args.prompt,
GEMINI_API_KEY: apiKey,
});
},
});
🧪 Testing
For testing with convex-test:
import nanoBananaTest from "convex-nano-banana/test";
import { convexTest } from "convex-test";
function initConvexTest() {
const t = convexTest();
nanoBananaTest.register(t);
return t;
}
test("Generate image", async () => {
const t = initConvexTest();
// ... your tests
});
🔧 Development
# Install dependencies
npm install
# Build the package
npm run build
# Run tests
npm test
# Type check
npm run typecheck
📄 License
Apache-2.0
🙏 Credits
Built with:
- Convex - The backend platform
- Google Gemini - AI image generation
- @google/genai - Gemini SDK
Made with 🍌 by the Convex community