# Nano Banana

Generate stunning AI images directly in your Convex backend with persistent storage, real-time status tracking, and a clean TypeScript API.

This is an upstream package. It has not been adapted or qualified for Bijection.

- Package: convex-nano-banana
- Source: https://github.com/dperussina/convex-nano-banana
- npm: https://www.npmjs.com/package/convex-nano-banana
- Directory: https://bijection.com/components/convex-nano-banana

Read the upstream README for setup, license, and compatibility before using it.

## Upstream package documentation

Recorded 2026-10-23. This describes convex-nano-banana; check the Bijection adaptation separately.

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

• **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

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.

### Upstream README

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

```bash
npm install convex-nano-banana
```

## 🚀 Quick Start

### 1. Add the component to your Convex app

Create or update `convex/convex.config.ts`:

```typescript
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`:

```typescript
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

```tsx
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

```typescript
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.

```typescript
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.

```typescript
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).

```typescript
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.

```typescript
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.

```typescript
const { generations, hasMore, cursor } = await nanoBanana.list(ctx, {
  userId: "user_123",
  limit: 20,
});
```

#### `listByStatus(ctx, { status, limit? })` → `Promise<Generation[]>`

List generations by status.

```typescript
const failed = await nanoBanana.listByStatus(ctx, {
  status: "failed",
  limit: 10,
});
```

#### `delete(ctx, { generationId })` → `Promise<void>`

Delete a generation and its stored images.

```typescript
await nanoBanana.delete(ctx, { generationId: "abc123" });
```

#### `deleteAllForUser(ctx, { userId })` → `Promise<{ deleted: number }>`

Delete all generations for a user.

```typescript
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)

```bash
# Set in Convex dashboard or .env.local
npx convex env set GEMINI_API_KEY your-key-here
```

```typescript
// 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:

```typescript
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`:

```typescript
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

```bash
# 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](https://convex.dev) - The backend platform
- [Google Gemini](https://ai.google.dev/) - AI image generation
- [@google/genai](https://www.npmjs.com/package/@google/genai) - Gemini SDK

---

Made with 🍌 by the Convex community