# How to Build Image hosting with V0

- Tool: v0
- Difficulty: Intermediate
- Compatibility: V0 Premium
- Last updated: April 2026

## TL;DR

Build an Imgur-style image hosting app with V0 using Next.js, Supabase Storage for uploads, and short-code shareable links. You'll create a drag-and-drop uploader, public gallery with masonry layout, image transformations, and album organization — all in about 1-2 hours without touching a terminal.

## Before you start

- A V0 account (Premium recommended for the multi-page app)
- A Supabase project with Storage enabled and Image Transformations turned on
- Basic understanding of image sharing (upload, share link, gallery)
- No advanced coding experience needed

## Step-by-step guide

### 1. Set up the project and image storage schema

Open V0 and create a new project. Connect Supabase via the Connect panel. Create the database schema and configure the Storage bucket for images.

```
// Paste this prompt into V0's AI chat:
// Build an image hosting app. Create a Supabase schema with:
// 1. images: id (uuid PK), user_id (uuid FK to auth.users nullable), filename (text), original_name (text), file_size (bigint), width (int), height (int), mime_type (text), storage_path (text), thumbnail_path (text), is_public (boolean default true), view_count (int default 0), short_code (text unique), created_at (timestamptz)
// 2. albums: id (uuid PK), user_id (uuid FK to auth.users), title (text), description (text), slug (text unique), is_public (boolean default true), created_at (timestamptz)
// 3. album_images: image_id (uuid FK to images), album_id (uuid FK to albums), position (int), PK(image_id, album_id)
// Create a PostgreSQL function generate_short_code() that creates a random 8-char alphanumeric string using encode(gen_random_bytes(6), 'base64') with retry on unique conflict
// Create a Supabase Storage bucket 'images' with 10MB file limit
// Add RLS: public images readable by all, authenticated users can upload and manage their own images
```

> Pro tip: Enable Image Transformations in the Supabase Dashboard under Storage settings — this lets you generate thumbnails and resize images via URL query parameters without any extra code.

**Expected result:** Database schema created with images, albums, and album_images tables. Storage bucket configured with 10MB limit and Image Transformations enabled.

### 2. Build the drag-and-drop uploader

Create the upload page with a multi-file drag-and-drop zone, upload progress tracking, and privacy toggle. Files go to Supabase Storage via an API route that generates short codes.

```
// Paste this prompt into V0's AI chat:
// Build an upload page at app/upload/page.tsx as a 'use client' component.
// Requirements:
// - Drag-and-drop zone that accepts multiple image files (PNG, JPG, GIF, WebP)
// - Click to browse fallback using Input type='file' with accept='image/*' multiple
// - Show file previews as thumbnails before upload
// - Switch toggle for public/private per image
// - Upload Progress bar per file showing percentage
// - On upload, call POST /api/upload with FormData containing:
//   the file, is_public flag
// - The API route (app/api/upload/route.ts) should:
//   Generate a short_code using the generate_short_code() PostgreSQL function
//   Upload the original to Supabase Storage at images/{user_id}/{filename}
//   Read image dimensions (use the file metadata or sharp if available)
//   Insert row into images table with all metadata
//   Return the short_code for the shareable link
// - After upload, show the shareable link /i/{short_code} with copy Button
// - Use Card to wrap the upload zone and Progress for upload status
```

**Expected result:** Users can drag and drop multiple images, see upload progress, set privacy, and receive shareable short-code links after upload completes.

### 3. Build the public gallery and image viewer

Create the homepage gallery showing public images in a masonry grid and the individual image page with metadata, sharing buttons, and direct link.

```
// Paste this prompt into V0's AI chat:
// Build two pages:
// 1. app/page.tsx — public gallery:
//    - Masonry grid layout using CSS columns (3 cols desktop, 2 tablet, 1 mobile)
//    - Card for each image thumbnail (300px width via Supabase Image Transformations URL)
//    - Hover overlay showing image dimensions and file size
//    - Select for sort order: recent (default), most viewed
//    - "Upload" Button as CTA
//    - Infinite scroll or pagination with 24 images per page
//
// 2. app/i/[short_code]/page.tsx — image viewer:
//    - Full-size image display
//    - Metadata Card: original name, dimensions, file size Badge, upload date, view count
//    - Share buttons: copy direct link, copy embed code (<img> tag), copy markdown
//    - Dialog for image details with download Button
//    - On-the-fly resize: links for common sizes (640px, 1024px, 1920px) using ?w= query param
//    - Increment view_count via Server Action on page load
//    - If image is private and viewer is not the owner, show 404
```

**Expected result:** The homepage shows a masonry gallery of public images. Individual image pages display full-size images with metadata, share buttons, and resize links.

### 4. Build the on-the-fly resize API and albums

Create the image resize API that serves images at custom dimensions and the album system for organizing images.

```
// Paste this prompt into V0's AI chat:
// Build image resize API and albums:
// 1. app/api/images/[short_code]/route.ts:
//    - GET handler that reads ?w (width) and ?q (quality, default 75) query params
//    - Looks up image by short_code
//    - Returns a redirect to the Supabase Storage transform URL:
//      {SUPABASE_URL}/storage/v1/render/image/public/images/{path}?width={w}&quality={q}
//    - If no params, redirect to the original file URL
//    - Cache headers: public, max-age=31536000 for resized images
//
// 2. app/album/[slug]/page.tsx — album view:
//    - Album title, description, image count Badge
//    - Image grid similar to gallery but filtered to album images ordered by position
//    - Share Button for the album URL
//
// 3. app/dashboard/page.tsx — user dashboard:
//    - Tabs: My Images (grid with delete Button per image), My Albums (list with edit/delete)
//    - Storage usage Card showing total bytes used
//    - "Create Album" Button opening Dialog with title Input, description Textarea, slug Input
//    - Drag images into albums from the image grid
//    - Server Actions for album CRUD and image-to-album assignment
```

> Pro tip: Supabase Image Transformations handle resizing at the CDN level — no server compute needed. Just append width and quality params to the storage URL.

**Expected result:** The resize API redirects to transformed Supabase Storage URLs with caching. Users can create albums, organize images, and manage everything from their dashboard.

### 5. Add short-code generation and sharing features

Create the PostgreSQL function for unique short codes and add embed code generation and social sharing.

```
// Paste this prompt into V0's AI chat:
// Add short code generation and sharing:
// 1. Create a PostgreSQL function generate_short_code() that:
//    Generates encode(gen_random_bytes(6), 'base64') → replace '+' with 'a', '/' with 'b', remove '='
//    Truncate to 8 characters
//    Check uniqueness against images.short_code
//    Retry up to 5 times if collision, raise exception if all fail
//    Returns the unique short_code
//
// 2. Update app/i/[short_code]/page.tsx to add:
//    - Open Graph meta tags: og:image pointing to the image URL, og:title = original_name
//    - Twitter card meta tags for image preview
//    - Embed code generator: Dialog showing copyable HTML, Markdown, and BBCode snippets
//    - Social share buttons for Twitter, Reddit, and direct link copy
//    - "Report" Button (stores report in a reports table)
//
// 3. Add generateMetadata function for SEO with dynamic og:image
```

**Expected result:** Short codes are generated uniquely via PostgreSQL. Image pages include OG meta tags for social previews and embed code generation for sharing.

## Complete code example

File: `app/api/upload/route.ts`

```typescript
import { createClient } from '@/lib/supabase/server'
import { NextRequest, NextResponse } from 'next/server'

export async function POST(request: NextRequest) {
  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })

  const formData = await request.formData()
  const file = formData.get('file') as File
  const isPublic = formData.get('is_public') === 'true'

  if (!file || !file.type.startsWith('image/')) {
    return NextResponse.json({ error: 'Invalid image file' }, { status: 400 })
  }

  const { data: shortCode } = await supabase.rpc('generate_short_code')
  const storagePath = `${user.id}/${shortCode}-${file.name}`

  const { error: uploadError } = await supabase.storage
    .from('images')
    .upload(storagePath, file, {
      contentType: file.type,
      upsert: false,
    })

  if (uploadError) {
    return NextResponse.json({ error: uploadError.message }, { status: 500 })
  }

  const { error: dbError } = await supabase.from('images').insert({
    user_id: user.id,
    filename: `${shortCode}-${file.name}`,
    original_name: file.name,
    file_size: file.size,
    mime_type: file.type,
    storage_path: storagePath,
    is_public: isPublic,
    short_code: shortCode,
  })

  if (dbError) {
    return NextResponse.json({ error: dbError.message }, { status: 500 })
  }

  return NextResponse.json({ short_code: shortCode })
}
```

## Common mistakes

- **Storing images in the database instead of Supabase Storage** — Binary data in PostgreSQL bloats the database, slows queries, and cannot leverage CDN caching or image transformations. Fix: Upload files to a Supabase Storage bucket and store only the storage_path in the database.
- **Generating short codes on the client side** — Client-generated codes can collide without a database uniqueness check, leading to duplicate URLs. Fix: Use a PostgreSQL function with a unique constraint and retry loop to guarantee uniqueness at the database level.
- **Not setting cache headers on resized images** — Without caching, every request to a resized image re-processes the transformation, wasting bandwidth and adding latency. Fix: Set Cache-Control: public, max-age=31536000 on the resize API response since resized versions of the same image never change.
- **Allowing unlimited file sizes** — Without a file size limit, users can upload massive images that consume storage quota and slow down the gallery. Fix: Set a 10MB file limit on the Supabase Storage bucket and validate file size client-side before upload.

## Best practices

- Upload files to Supabase Storage and store only the path in the database — never store binary data in PostgreSQL.
- Use Supabase Image Transformations for thumbnails and resizing via URL parameters instead of server-side processing.
- Generate short codes with a PostgreSQL function using gen_random_bytes for randomness and a unique constraint for safety.
- Set long-lived cache headers (max-age=31536000) on resized images since transformed versions are immutable.
- Validate file types and sizes both client-side (for UX) and server-side (for security) in the upload API route.
- Use Open Graph meta tags with the image URL so shared links preview correctly on social media and messaging apps.

## Frequently asked questions

### Can I build this with the free V0 plan?

The core upload and gallery pages work with the free tier. V0 Premium is recommended for the full multi-page app with albums and dashboard.

### How do short codes work?

A PostgreSQL function generates a random 8-character alphanumeric string using gen_random_bytes. A unique constraint ensures no collisions, and the function retries up to 5 times if a duplicate is generated.

### Can I resize images on the fly?

Yes. Enable Image Transformations in the Supabase Dashboard under Storage settings. Then append ?width=800&quality=75 to any storage URL to get a resized version served from the CDN.

### What's the maximum file size?

The Supabase Storage bucket is configured with a 10MB limit. You can increase this in the bucket settings, but consider the impact on storage costs and page load times.

### Can anonymous users upload?

The current setup requires authentication for uploads. To allow anonymous uploads, make user_id nullable in the images table and adjust the RLS policies accordingly.

### How do I deploy?

Publish via V0's Share menu. Set NEXT_PUBLIC_SUPABASE_ANON_KEY and NEXT_PUBLIC_SUPABASE_URL for the client-side gallery, and SUPABASE_SERVICE_ROLE_KEY for the upload API route.

### Can RapidDev help build a custom image platform?

Yes. RapidDev has built 600+ apps including media platforms with CDN optimization, watermarking, and advanced content moderation. Book a free consultation to discuss your project.

---

Source: https://www.rapidevelopers.com/how-to-build-v0/image-hosting
© RapidDev — https://www.rapidevelopers.com/how-to-build-v0/image-hosting
