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 migrationsupabase 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.sqlCREATE 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 databasesupabase db reset# Push to remote (when ready)supabase db push
Manual Creation + Introspection Workflow
Step 1: Create Tables in Supabase Studio
- Go to
http://127.0.0.1:54323 - Navigate to Table Editor
- Create tables manually with UI
- Set up relationships and constraints
Step 2: Generate Migration from Existing Schema
# Pull schema from local databasesupabase db diff --schema public > supabase/migrations/20240116123456_manual_schema.sql# Or pull from remotesupabase db pull
How to Make Me Aware of Your Schema
Method 1: Share Migration Files
# Show me your migration filescat supabase/migrations/*.sql
Method 2: Database Introspection
# Generate current schemasupabase db diff --schema public# Or show specific table structuresupabase db diff --schema public --table photos
Method 3: Export Schema Documentation
# Generate schema documentationsupabase 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 changessupabase db diff --schema public > temp_schema.sql# 2. Review and clean up the generated SQL# 3. Create proper migration filesupabase migration new stable_schema# Copy cleaned SQL into migration file# 4. Reset and test with migrationssupabase 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 sharingCREATE TABLE public.photos ( -- schema here);
Environment Management
# Local developmentsupabase startsupabase db reset# Stagingsupabase link --project-ref your-staging-projectsupabase db push# Productionsupabase link --project-ref your-production-projectsupabase db push
Schema Documentation
// File: types/database.ts// Auto-generated from: supabase gen types typescript --localexport 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 schemasupabase db diff --schema public# Generate TypeScript typessupabase gen types typescript --local# Show migration historyls -la supabase/migrations/# Show specific table structuresupabase 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!
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
Post a Comment