The Complete Guide to Building with Supabase
Features • Quick Start • Docs • Contributing
A complete, production-ready reference for building applications with Supabase. Not just a todo app—this showcases every major Supabase feature with real-world examples you can copy directly into your projects.
Perfect for:
- 🎓 Learning Supabase from scratch
- 📚 Reference when building your own apps
- 🚀 Starter template for new projects
- 👥 Teaching others how to use Supabase
Live Demo: [Coming Soon] | Video Tutorial: [Coming Soon]
- ✅ Email/password signup & login
- ✅ Magic links (passwordless authentication)
- ✅ OAuth (GitHub, Google)
- ✅ Session management & persistence
- ✅ Protected routes
- ✅ Sign out
- ✅ Full CRUD (Create, Read, Update, Delete)
- ✅ TypeScript types generated from schema
- ✅ Filtering (
eq,gte,lte,ilike,or) - ✅ Ordering & sorting
- ✅ Pagination (
range) - ✅ Joins & nested selects (foreign key relationships)
- ✅ Counting & aggregations
- ✅ Upsert operations
- ✅ User-specific data policies
- ✅ Public vs private data
- ✅ INSERT/UPDATE/DELETE restrictions
- ✅ Live demo of policy enforcement
- ✅ Subscribe to INSERT/UPDATE/DELETE events
- ✅ Live UI updates across clients
- ✅ Presence (who's online)
- ✅ File uploads (images, documents)
- ✅ Public vs protected buckets
- ✅ File validation (type, size)
- ✅ Public URL generation
- ✅ Signed URLs for private files
- ✅ Service layer ready for edge function calls
- ✅ Examples for custom business logic
- ✅ Payment processing patterns
- ✅ Webhook handlers
- ✅ Call custom Postgres functions
- ✅ Complex queries & aggregations
- ✅ Transaction-like behavior
- ✅ Return tables or JSON
supabase-app/
├── src/
│ ├── lib/
│ │ ├── supabaseClient.ts # Supabase client initialization
│ │ └── database.types.ts # TypeScript types from schema
│ ├── hooks/
│ │ ├── useAuth.ts # Authentication hooks & functions
│ │ └── useRealtime.ts # Realtime subscription hooks
│ ├── services/
│ │ ├── libraryService.ts # CRUD operations for library/books
│ │ ├── storageService.ts # File upload/download functions
│ │ ├── rpcService.ts # RPC (Postgres function) calls
│ │ └── edgeFunctionsService.ts # Edge function wrappers
│ ├── components/
│ │ ├── AuthForm.tsx # Email/OAuth/Magic link auth UI
│ │ ├── LibraryManager.tsx # Full CRUD demo with UI
│ │ ├── RealtimeMessages.tsx # Live message feed
│ │ ├── FileUpload.tsx # File upload with preview
│ │ └── RLSDemo.tsx # RLS policy demonstration
│ ├── App.tsx # Main app with tab navigation
│ └── main.tsx # App entry point
├── .env.local # Environment variables (create this)
└── README.md
- Separation of Concerns: Hooks for state, services for data, components for UI
- TypeScript First: Full type safety from database schema
- Real-world Examples: Practical patterns you can copy directly
- Commented Code: Every feature explained inline
- Error Handling: Proper error states and loading indicators
Get up and running in 10 minutes! See QUICKSTART.md for detailed instructions.
# 1. Clone the repository
git clone https://github.com/yourusername/supabase-react-showcase.git
cd supabase-react-showcase
# 2. Install dependencies
npm install
# 3. Set up environment variables
cp .env.local.template .env.local
# Edit .env.local with your Supabase credentials
# 4. Run the app
npm run devThen set up your Supabase database using supabase-setup.sql - full instructions in QUICKSTART.md!
- Node.js 18+ and npm/yarn
- A Supabase account (supabase.com)
cd supabase-app
npm installCreate a .env.local file in the root:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-hereGet these from your Supabase dashboard: Settings > API
Run these SQL commands in your Supabase SQL Editor (Database > SQL Editor):
-- Library table
CREATE TABLE library (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
address VARCHAR(100) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Book table
CREATE TABLE book (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
pages INTEGER NOT NULL,
release DATE NOT NULL,
locatedIn_id INTEGER NOT NULL REFERENCES library(id) ON DELETE CASCADE,
cover_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Author table
CREATE TABLE author (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
user_id UUID REFERENCES auth.users(id),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Author-Book junction table (many-to-many)
CREATE TABLE author_book (
publishes INTEGER NOT NULL REFERENCES book(id) ON DELETE CASCADE,
"writtenBy" INTEGER NOT NULL REFERENCES author(id) ON DELETE CASCADE,
PRIMARY KEY (publishes, "writtenBy")
);CREATE TABLE user_profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT UNIQUE NOT NULL,
bio TEXT,
avatar_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE user_profiles ENABLE ROW LEVEL SECURITY;
-- RLS Policies
CREATE POLICY "Profiles are viewable by everyone"
ON user_profiles FOR SELECT
USING (true);
CREATE POLICY "Users can insert their own profile"
ON user_profiles FOR INSERT
WITH CHECK (auth.uid() = id);
CREATE POLICY "Users can update their own profile"
ON user_profiles FOR UPDATE
USING (auth.uid() = id);
CREATE POLICY "Users can delete their own profile"
ON user_profiles FOR DELETE
USING (auth.uid() = id);CREATE TABLE messages (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Enable Realtime
ALTER PUBLICATION supabase_realtime ADD TABLE messages;
-- Optional: Enable RLS for user-specific messages
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can read their own messages"
ON messages FOR SELECT
USING (auth.uid()::text = user_id);
CREATE POLICY "Users can insert their own messages"
ON messages FOR INSERT
WITH CHECK (auth.uid()::text = user_id);
CREATE POLICY "Users can delete their own messages"
ON messages FOR DELETE
USING (auth.uid()::text = user_id);-- Count books in a library
CREATE OR REPLACE FUNCTION get_books_count_by_library(library_id INTEGER)
RETURNS INTEGER
LANGUAGE plpgsql
AS $$
DECLARE
book_count INTEGER;
BEGIN
SELECT COUNT(*)
INTO book_count
FROM book
WHERE "locatedIn_id" = library_id;
RETURN book_count;
END;
$$;
-- Search books by title
CREATE OR REPLACE FUNCTION search_books(search_term TEXT)
RETURNS SETOF book
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT *
FROM book
WHERE title ILIKE '%' || search_term || '%'
ORDER BY title;
END;
$$;
-- Get library statistics
CREATE OR REPLACE FUNCTION get_library_stats(library_id INTEGER)
RETURNS JSON
LANGUAGE plpgsql
AS $$
DECLARE
result JSON;
BEGIN
SELECT json_build_object(
'total_books', COUNT(*),
'total_pages', COALESCE(SUM(pages), 0),
'avg_pages', COALESCE(AVG(pages), 0),
'oldest_book', MIN(release),
'newest_book', MAX(release)
)
INTO result
FROM book
WHERE "locatedIn_id" = library_id;
RETURN result;
END;
$$;Go to Database > Replication:
- Enable replication for
messagestable - This allows real-time subscriptions
Go to Authentication > Providers:
- Email: Enable email provider
- Magic Link: Enable passwordless sign-in (optional)
- GitHub OAuth: Add GitHub OAuth app credentials (optional)
- Google OAuth: Add Google OAuth credentials (optional)
Go to Storage > Create bucket:
- Create
avatarsbucket (public) - Create
book-coversbucket (public)
For each bucket, toggle Public bucket to ON.
# Install Supabase CLI
npm install -g supabase
# Login
supabase login
# Generate types
npx supabase gen types typescript --project-id your-project-id > src/lib/database.types.tsnpm run dev- Sign Up: Users enter email/password → confirmation email sent
- Sign In: Email/password OR magic link OR OAuth
- Session: Auto-persisted in localStorage
- Protected Content: App shows features only when authenticated
- Create:
supabase.from('table').insert(data) - Read:
supabase.from('table').select('*') - Update:
supabase.from('table').update(data).eq('id', id) - Delete:
supabase.from('table').delete().eq('id', id)
const channel = supabase
.channel('my-channel')
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages' }, (payload) => {
console.log('Change received!', payload)
})
.subscribe()const { data, error } = await supabase.storage
.from('bucket-name')
.upload('path/to/file.jpg', fileObject)const { data, error } = await supabase.rpc('my_function', {
param1: value1,
param2: value2,
})- QUICKSTART.md - Get running in 10 minutes
- FEATURES.md - Complete feature reference with code locations
- CONTRIBUTING.md - How to contribute
- Supabase Docs - Official documentation
Contributions are welcome! Whether it's:
- 🐛 Bug fixes
- ✨ New features or examples
- 📝 Documentation improvements
- 💡 Suggestions
Please read CONTRIBUTING.md for guidelines.
Thanks to all the amazing people who have contributed to this project!
If this project helped you, please consider:
- ⭐ Starring the repository
- 🐦 Sharing on Twitter
- 📝 Writing a blog post about it
- 💬 Telling others who might find it useful
This project is MIT licensed. Use it freely in your projects!
- Supabase - For building an amazing platform
- Vite - For the blazing fast dev experience
- Community - For feedback and contributions
Built with ❤️ for the Supabase community