npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

convex-nano-banana

v0.1.0

Published

AI Image Generation Component for Convex powered by Gemini

Downloads

4,987

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.

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

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


Made with 🍌 by the Convex community