
npm install convex-nano-bananaAI 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.
npm install convex-nano-bananaCreate 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;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);
},
});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 && (
<img src={generation.imageUrl} alt={generation.prompt} />
)}
{generation?.status === "failed" && (
<p>โ Error: {generation.error}</p>
)}
</div>
);
}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
});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",
});| 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!
Supported values: "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"
For gemini-3-pro-image-preview: "1K", "2K", "4K"
| Status | Description |
|---|---|
pending | Created but not started |
generating | Currently generating |
complete | Successfully generated |
failed | Generation failed (check error field) |
Never expose your Gemini API key to clients!
# 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,
});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,
});
},
});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
});# Install dependencies
npm install
# Build the package
npm run build
# Run tests
npm test
# Type check
npm run typecheckApache-2.0
Built with:
Made with ๐ by the Convex community