Supabase Database Schema Management: Migration-First vs Manual Creation

 

I asked Cursor: For my app, i will have a camera, and have users store images in bucket and allow people to share messages about the photos. Do not create this now, but how do we create the sql for this?

Great question! Let me outline the SQL schema design for your camera app with image storage and photo sharing/messaging features. I'll show you how to structure this with Supabase's storage and database capabilities.

📸 Camera App Database Schema Design

1. Storage Buckets (Supabase Storage)

First, you'll need to configure storage buckets in your supabase/config.toml:

# Image storage bucket
[storage.buckets.photos]
public = false  # Private bucket for user photos
file_size_limit = "10MiB"
allowed_mime_types = ["image/png", "image/jpeg", "image/webp"]
objects_path = "./photos"

# Thumbnails bucket
[storage.buckets.thumbnails]
public = true  # Public bucket for thumbnails
file_size_limit = "1MiB"
allowed_mime_types = ["image/png", "image/jpeg", "image/webp"]
objects_path = "./thumbnails"

2. Database Tables SQL

Here's the complete SQL schema for your camera app:

-- Enable necessary extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Photos table
CREATE TABLE public.photos (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  title TEXT,
  description TEXT,
  file_path TEXT NOT NULL, -- Path in storage bucket
  thumbnail_path TEXT, -- Path to thumbnail
  file_size INTEGER NOT NULL,
  mime_type TEXT NOT NULL,
  width INTEGER,
  height INTEGER,
  location_lat DECIMAL(10, 8), -- Latitude
  location_lng DECIMAL(11, 8), -- Longitude
  location_name TEXT, -- Human-readable location
  camera_settings JSONB, -- Store camera settings as JSON
  tags TEXT[], -- Array of tags
  is_public BOOLEAN DEFAULT false,
  is_archived BOOLEAN DEFAULT false,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);

-- Comments/Messages table
CREATE TABLE public.photo_comments (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  photo_id UUID REFERENCES public.photos(id) ON DELETE CASCADE NOT NULL,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  parent_comment_id UUID REFERENCES public.photo_comments(id) ON DELETE CASCADE, -- For replies
  content TEXT NOT NULL,
  is_edited BOOLEAN DEFAULT false,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);

-- Photo sharing table
CREATE TABLE public.photo_shares (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  photo_id UUID REFERENCES public.photos(id) ON DELETE CASCADE NOT NULL,
  shared_by_user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  shared_with_user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, -- NULL for public shares
  share_token TEXT UNIQUE, -- For public sharing via link
  permissions TEXT[] DEFAULT ARRAY['view'], -- ['view', 'comment', 'download']
  expires_at TIMESTAMP WITH TIME ZONE, -- Optional expiration
  is_active BOOLEAN DEFAULT true,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);

-- Photo likes/reactions table
CREATE TABLE public.photo_likes (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  photo_id UUID REFERENCES public.photos(id) ON DELETE CASCADE NOT NULL,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  reaction_type TEXT DEFAULT 'like', -- 'like', 'love', 'laugh', etc.
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
  UNIQUE(photo_id, user_id) -- One reaction per user per photo
);

-- Photo collections/albums
CREATE TABLE public.photo_collections (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
  name TEXT NOT NULL,
  description TEXT,
  cover_photo_id UUID REFERENCES public.photos(id),
  is_public BOOLEAN DEFAULT false,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);

-- Many-to-many relationship between photos and collections
CREATE TABLE public.collection_photos (
  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
  collection_id UUID REFERENCES public.photo_collections(id) ON DELETE CASCADE NOT NULL,
  photo_id UUID REFERENCES public.photos(id) ON DELETE CASCADE NOT NULL,
  added_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
  UNIQUE(collection_id, photo_id)
);

-- User profiles (extend existing auth.users)
CREATE TABLE public.user_profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  username TEXT UNIQUE,
  display_name TEXT,
  bio TEXT,
  avatar_url TEXT,
  website TEXT,
  location TEXT,
  is_public BOOLEAN DEFAULT true,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);

3. Indexes for Performance

-- Photos indexes
CREATE INDEX idx_photos_user_id ON public.photos(user_id);
CREATE INDEX idx_photos_created_at ON public.photos(created_at DESC);
CREATE INDEX idx_photos_is_public ON public.photos(is_public) WHERE is_public = true;
CREATE INDEX idx_photos_location ON public.photos USING GIST (ST_Point(location_lng, location_lat));
CREATE INDEX idx_photos_tags ON public.photos USING GIN(tags);

-- Comments indexes
CREATE INDEX idx_photo_comments_photo_id ON public.photo_comments(photo_id);
CREATE INDEX idx_photo_comments_user_id ON public.photo_comments(user_id);
CREATE INDEX idx_photo_comments_created_at ON public.photo_comments(created_at DESC);

-- Shares indexes
CREATE INDEX idx_photo_shares_photo_id ON public.photo_shares(photo_id);
CREATE INDEX idx_photo_shares_shared_with ON public.photo_shares(shared_with_user_id);
CREATE INDEX idx_photo_shares_token ON public.photo_shares(share_token) WHERE share_token IS NOT NULL;

-- Likes indexes
CREATE INDEX idx_photo_likes_photo_id ON public.photo_likes(photo_id);
CREATE INDEX idx_photo_likes_user_id ON public.photo_likes(user_id);

4. Row Level Security (RLS) Policies

-- Enable RLS
ALTER TABLE public.photos ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.photo_comments ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.photo_shares ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.photo_likes ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.photo_collections ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.user_profiles ENABLE ROW LEVEL SECURITY;

-- Photos policies
CREATE POLICY "Users can view their own photos" ON public.photos
  FOR SELECT USING (auth.uid() = user_id);

CREATE POLICY "Users can view public photos" ON public.photos
  FOR SELECT USING (is_public = true);

CREATE POLICY "Users can insert their own photos" ON public.photos
  FOR INSERT WITH CHECK (auth.uid() = user_id);

CREATE POLICY "Users can update their own photos" ON public.photos
  FOR UPDATE USING (auth.uid() = user_id);

-- Comments policies
CREATE POLICY "Users can view comments on photos they can see" ON public.photo_comments
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM public.photos 
      WHERE id = photo_id 
      AND (user_id = auth.uid() OR is_public = true)
    )
  );

CREATE POLICY "Users can insert comments" ON public.photo_comments
  FOR INSERT WITH CHECK (auth.uid() = user_id);

5. Database Functions for Common Operations

-- Function to get photo with metadata
CREATE OR REPLACE FUNCTION get_photo_with_metadata(photo_uuid UUID)
RETURNS TABLE (
  photo public.photos,
  comment_count BIGINT,
  like_count BIGINT,
  user_liked BOOLEAN
) AS $$
BEGIN
  RETURN QUERY
  SELECT 
    p.*,
    COALESCE(comment_counts.count, 0) as comment_count,
    COALESCE(like_counts.count, 0) as like_count,
    COALESCE(user_likes.liked, false) as user_liked
  FROM public.photos p
  LEFT JOIN (
    SELECT photo_id, COUNT(*) as count
    FROM public.photo_comments
    GROUP BY photo_id
  ) comment_counts ON p.id = comment_counts.photo_id
  LEFT JOIN (
    SELECT photo_id, COUNT(*) as count
    FROM public.photo_likes
    GROUP BY photo_id
  ) like_counts ON p.id = like_counts.photo_id
  LEFT JOIN (
    SELECT photo_id, true as liked
    FROM public.photo_likes
    WHERE user_id = auth.uid()
  ) user_likes ON p.id = user_likes.photo_id
  WHERE p.id = photo_uuid;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Function to generate share token
CREATE OR REPLACE FUNCTION generate_share_token()
RETURNS TEXT AS $$
BEGIN
  RETURN encode(gen_random_bytes(32), 'base64url');
END;
$$ LANGUAGE plpgsql;

6. Storage Policies

-- Storage policies for photos bucket
CREATE POLICY "Users can upload their own photos" ON storage.objects
  FOR INSERT WITH CHECK (
    bucket_id = 'photos' 
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

CREATE POLICY "Users can view their own photos" ON storage.objects
  FOR SELECT USING (
    bucket_id = 'photos' 
    AND auth.uid()::text = (storage.foldername(name))[1]
  );

-- Public thumbnails policy
CREATE POLICY "Anyone can view thumbnails" ON storage.objects
  FOR SELECT USING (bucket_id = 'thumbnails');

7. Migration File Structure

You would create this as a migration file in supabase/migrations/:

-- File: supabase/migrations/20240116000000_create_camera_app_schema.sql
-- (The complete SQL above would go here)

8. Key Features This Schema Supports

  • Photo storage with metadata (location, camera settings, tags)
  • Comments and replies on photos
  • Photo sharing (private/public, with permissions)
  • Likes and reactions
  • Photo collections/albums
  • User profiles
  • Location-based queries (using PostGIS)
  • Full-text search on tags and descriptions
  • Row-level security for data protection

This schema provides a solid foundation for a camera app with social features while leveraging Supabase's storage and database capabilities efficiently!


This schema provides a solid foundation for a camera app with social features while leveraging Supabase's storage and database capabilities efficiently!


I followed up with Cursor: When developing this kind of app, and we determine the data model and tables. is that a separate process that is migrated to the database? Or could I manually create the database tables and joins in supabase? If I do, how can I make you aware of the structure so we can develop app locally? Explain this process and best practises.

Database Schema Management Process

Two Approaches - Choose Your Workflow

Option A: Migration-First (Recommended)

  • Define schema in SQL migration files
  • Version-controlled database changes
  • Reproducible across environments
  • Team collaboration friendly

Option B: Manual Creation + Introspection

  • Create tables manually in Supabase Studio
  • Generate migration files from existing schema
  • Good for rapid prototyping
  • Can be converted to migration-first later

Migration-First Workflow (Best Practice)

Step 1: Create Migration Files

# Create a new migration
supabase migration new create_camera_app_schema

# This creates: supabase/migrations/20240116123456_create_camera_app_schema.sql

Step 2: Write Your Schema

-- File: supabase/migrations/20240116123456_create_camera_app_schema.sql
CREATE TABLE public.photos (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
title TEXT,
file_path TEXT NOT NULL,
-- ... rest of schema
);

Step 3: Apply Migrations

# Apply to local database
supabase db reset

# Push to remote (when ready)
supabase db push

Manual Creation + Introspection Workflow

Step 1: Create Tables in Supabase Studio

  1. Go to http://127.0.0.1:54323
  2. Navigate to Table Editor
  3. Create tables manually with UI
  4. Set up relationships and constraints

Step 2: Generate Migration from Existing Schema

# Pull schema from local database
supabase db diff --schema public > supabase/migrations/20240116123456_manual_schema.sql

# Or pull from remote
supabase db pull

How to Make Me Aware of Your Schema

Method 1: Share Migration Files

# Show me your migration files
cat supabase/migrations/*.sql

Method 2: Database Introspection

# Generate current schema
supabase db diff --schema public

# Or show specific table structure
supabase db diff --schema public --table photos

Method 3: Export Schema Documentation

# Generate schema documentation
supabase gen types typescript --local > types/database.ts

Recommended Development Process

Phase 1: Rapid Prototyping

# 1. Create tables manually in Studio
# 2. Test your app functionality
# 3. Iterate quickly on schema changes

Phase 2: Schema Stabilization

# 1. Generate migration from manual changes
supabase db diff --schema public > temp_schema.sql

# 2. Review and clean up the generated SQL
# 3. Create proper migration file
supabase migration new stable_schema
# Copy cleaned SQL into migration file

# 4. Reset and test with migrations
supabase db reset

Phase 3: Production Ready

# 1. All changes go through migrations
# 2. Version control all schema changes
# 3. Test migrations on staging
# 4. Deploy to production

Best Practices

Schema Versioning

-- File: supabase/migrations/20240116123456_create_camera_app_schema.sql
-- Migration: Create camera app schema
-- Version: 1.0.0
-- Author: Your Name
-- Description: Initial schema for camera app with photos, comments, and sharing

CREATE TABLE public.photos (
-- schema here
);

Environment Management

# Local development
supabase start
supabase db reset

# Staging
supabase link --project-ref your-staging-project
supabase db push

# Production
supabase link --project-ref your-production-project
supabase db push

Schema Documentation

// File: types/database.ts
// Auto-generated from: supabase gen types typescript --local

export interface Database {
public: {
Tables: {
photos: {
Row: {
id: string;
user_id: string;
title: string | null;
file_path: string;
created_at: string;
};
// ... other tables
};
};
};
}

Commands to Share Schema with Me

# Generate complete schema
supabase db diff --schema public

# Generate TypeScript types
supabase gen types typescript --local

# Show migration history
ls -la supabase/migrations/

# Show specific table structure
supabase db diff --schema public --table photos

My Recommendation

Start with manual creation in Studio for rapid prototyping, then generate migrations once you're happy with the schema. This gives you the best of both worlds - speed and reproducibility!

These commands have been tested and work well. 
 1. Create a new schema file:

supabase migration new wpp-schema

It generated this: supabase/migrations/20250917032101_wpp-schema.sql

2. Export Schema from Local Supabase

Requires PostGres CLI tools from this: https://www.postgresql.org/download/windows/
(install only CLI tools)

& "C:\Program Files\PostgreSQL\17\bin\pg_dump.exe" --schema-only --no-owner --no-privileges --dbname "postgresql://postgres:postgres@localhost:54328/postgres" > schema.sql

This creates a file called schema.sql in root folder.

3. Or run this command to export a schema from local supabase and copy it into the generated schema file.

This command was provided by cursor after it detected the blank schema file I generated above:

supabase db diff --schema public > supabase/migrations/20250917032101_wpp-schema.sql

Comments