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

supaphoto

v1.0.2

Published

CLI tool and library to save photos to Supabase with automatic metadata extraction

Downloads

276

Readme

SupaPhoto

A cost-effective CLI tool and Node.js library to save photos to Supabase with automatic metadata extraction.

Use it as a command-line tool for quick uploads, or import it as a library in your Node.js projects.

Architecture

  • Photos: Stored in Supabase Storage (cheap object storage ~$0.021/GB/month)
  • Metadata: Stored in PostgreSQL table (prompts, tags, dates, etc.)
  • Cost: Free tier includes 1GB storage + 2GB bandwidth/month

Setup

1. Install Dependencies

Already done! Dependencies are installed.

2. Configure Environment Variables

Update .env with your Supabase credentials:

SUPABASE_URL="https://your-project.supabase.co"
SUPABASE_KEY="your-supabase-key-here"

Get your key:

  1. Go to https://app.supabase.com
  2. Select your project
  3. Settings > API
  4. Copy either:
    • anon public key (for client-side use)
    • service_role secret key (recommended for CLI, has full access)

3. Set Up Database

Run the SQL in schema.sql in your Supabase SQL Editor:

  1. Go to https://app.supabase.com
  2. Select your project
  3. SQL Editor (left sidebar)
  4. Copy and paste contents of schema.sql
  5. Click "Run"

4. Create Storage Bucket

Follow instructions in setup-storage.md:

  1. Go to Storage section in Supabase Dashboard
  2. Create a new bucket named photos
  3. Choose public or private based on your needs

5. Install CLI Globally

Already done with npm link!

Usage

As a Library

Install in your project:

npm install supaphoto

Use in your code:

const { savePhoto, createSupabaseClient, extractImageMetadata } = require('supaphoto');

// Initialize Supabase client
const supabase = createSupabaseClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_KEY
);

// Save a photo
async function uploadPhoto() {
  try {
    const result = await savePhoto(supabase, '/path/to/photo.jpg', {
      prompt: 'A beautiful landscape',
      metadata: { model: 'dall-e-3', style: 'photorealistic' }
    });

    console.log('Photo saved!', result);
    console.log('ID:', result.id);
    console.log('Public URL:', result.publicUrl);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

// Or just extract metadata without saving
async function getMetadata() {
  const metadata = await extractImageMetadata('/path/to/photo.jpg');
  console.log('Prompt:', metadata.prompt);
  console.log('All metadata:', metadata.metadata);
}

API:

  • createSupabaseClient(url, key) - Creates a Supabase client
  • savePhoto(supabase, filePath, options) - Saves a photo with automatic metadata extraction
    • options.prompt - Override extracted prompt
    • options.metadata - Additional metadata (object or JSON string)
    • options.bucket - Storage bucket name (default: 'photos')
    • Returns: {id, filename, storage_path, prompt, metadata, created_at, publicUrl, extractedPrompt}
  • extractImageMetadata(filePath) - Extract metadata without saving
    • Returns: {prompt, metadata}
  • isDirectory(filePath) - Check if a path is a directory
    • Returns: boolean
  • isImageFile(filePath) - Check if a file is a supported image type
    • Returns: boolean
  • findImageFiles(dirPath) - Recursively find all image files in a directory
    • Returns: string[] (sorted array of absolute file paths)
  • SUPPORTED_IMAGE_EXTENSIONS - Set of supported image extensions
    • Value: Set(['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.tiff', '.tif'])

As a CLI

Save a photo

supaphoto --save /path/to/photo.jpg

Save with a prompt/description

supaphoto --save photo.jpg --prompt "A beautiful sunset over the mountains"

Save with metadata

supaphoto --save photo.jpg --prompt "AI generated image" --metadata '{"model":"dall-e-3","style":"photorealistic"}'

Short options

supaphoto -s photo.jpg -p "My prompt" -m '{"key":"value"}'

Upload an entire directory (recursive)

Upload all images in a directory and its subdirectories:

supaphoto --save /path/to/photo-directory

Features:

  • Recursively processes all subdirectories
  • Continues on errors (shows summary at end)
  • Uploads one file at a time (serial processing)
  • Each file uses only its extracted metadata
  • --prompt and --metadata flags are ignored for directory uploads

Example output:

Scanning directory: /Users/petter/photos
Found 10 image file(s) to upload

[1/10] Processing sunset.jpg...
[1/10] ✓ Uploaded sunset.jpg
[1/10]   Found embedded prompt

[2/10] Processing mountain.png...
[2/10] ✓ Uploaded mountain.png

============================================================
UPLOAD SUMMARY
============================================================
Total files processed: 10
✓ Successful uploads: 9
✗ Failed uploads: 1

Successfully uploaded:
  ✓ sunset.jpg
    ID: 123e4567-e89b-12d3-a456-426614174000
    Prompt: A beautiful sunset over the ocean...
  ...

Failed uploads:
  ✗ corrupted.jpg
    Error: Upload error: Invalid image format

============================================================

Automatic Metadata Extraction

The CLI automatically extracts metadata from your images, including prompts that were embedded by AI image generation tools!

Supported metadata formats:

  • EXIF (JPEG, TIFF, etc.)
  • IPTC (image description fields)
  • PNG metadata chunks
  • XMP data

Prompt detection: The CLI looks for prompts in common fields used by popular tools:

  • Stable Diffusion (stores in PNG parameters field)
  • DALL-E (often in ImageDescription)
  • Midjourney (varies)
  • Other tools using standard EXIF/IPTC fields

How it works:

# If the image has embedded metadata, it will be automatically extracted
supaphoto --save ai-generated.png
# Output: ✓ Found prompt in image metadata

# You can override the extracted prompt
supaphoto --save ai-generated.png --prompt "My custom description"

# All extracted metadata is stored in the database
# User-provided metadata takes precedence over extracted

Examples

# Simple save
supaphoto --save vacation.jpg

# With prompt
supaphoto --save portrait.png --prompt "Professional headshot"

# With full metadata
supaphoto --save generated.png \
  --prompt "A cyberpunk cityscape at night" \
  --metadata '{"model":"midjourney","version":"v6","seed":12345}'

What Happens When You Save a Photo?

  1. Metadata extraction: The CLI reads EXIF/IPTC/PNG metadata from the image
    • Automatically detects prompts from AI image generators
    • Extracts camera info, creation date, software used, etc.
  2. Upload: Photo is uploaded to Supabase Storage (photos bucket)
  3. Database record: Created with:
    • Unique ID
    • Filename
    • Storage path
    • Prompt (extracted or provided)
    • Metadata (merged: extracted + user-provided)
    • Timestamp
  4. Returns: Photo ID and public URL (if bucket is public)

Database Schema

The photos table stores:

  • id - Unique UUID
  • filename - Original filename
  • storage_path - Path in storage bucket
  • prompt - Text description/prompt
  • metadata - JSONB field for flexible metadata
  • created_at - Timestamp

Cost Optimization

This setup is cost-effective because:

  • Binary photo data stored in cheap object storage (not database)
  • Database only stores lightweight metadata records
  • Free tier covers most personal use cases
  • Scales efficiently as you add more photos

Troubleshooting

"SUPABASE_URL and SUPABASE_KEY must be set"

  • Check your .env file has correct values
  • Make sure you replaced your-actual-key-here with real key

"Upload error: Bucket not found"

  • Create the photos bucket in Supabase Dashboard (see setup-storage.md)

"Database error"

  • Run the SQL in schema.sql to create the table

"Permission denied"

  • Use service_role key instead of anon key
  • Check RLS policies in Supabase