RealtimeKeep your app up to date
AuthenticationOver 80+ OAuth integrations
Convex Components
ComponentsIndependent, modular, TypeScript building blocks for your backend.
Open sourceSelf host and develop locally
AI CodingGenerate high quality Convex code with AI
Compare
Convex vs. Firebase
Convex vs. Supabase
Convex vs. SQL
DocumentationGet started with your favorite frameworks
SearchSearch across Docs, Stack, and Discord
TemplatesUse a recipe to get started quickly
Convex for StartupsStart and scale your company with Convex
Convex ChampionsAmbassadors that support our thriving community
Convex CommunityShare ideas and ask for help in our community Discord
Stack
Stack

Stack is the Convex developer portal and blog, sharing bright ideas and techniques for building with Convex.

Explore Stack
BlogDocsPricing
GitHub
Log inStart building
Back to Components

Nano Banana

dperussina's avatar
dperussina/convex-nano-banana
View repo
GitHub logoView package

Category

Backend
Nano Banana hero image
npm install convex-nano-banana

๐ŸŒ 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.

npm version License

โœจ 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 && (
        <img src={generation.imageUrl} alt={generation.prompt} />
      )}

      {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#

ModelDescriptionResolution
gemini-2.5-flash-imageFast generation (default)1024px
gemini-3-pro-image-previewHigh quality, up to 4K4096px

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#

StatusDescription
pendingCreated but not started
generatingCurrently generating
completeSuccessfully generated
failedGeneration 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

Get your app up and running in minutes
Start building
Convex logo
ProductSyncRealtimeAuthOpen sourceAI codingChefFAQPricing
DevelopersDocsBlogComponentsTemplatesStartupsChampionsChangelogPodcastLLMs.txt
CompanyAbout usBrandInvestorsBecome a partnerJobsNewsEventsTerms of servicePrivacy policySecurity
SocialTwitterDiscordYouTubeLumaLinkedInGitHub
A Trusted Solution
  • SOC 2 Type II Compliant
  • HIPAA Compliant
  • GDPR Verified
ยฉ2026 Convex, Inc.