--- name: supabase-examples description: Full-stack templates and working examples for Supabase applications parent-skill: moai-platform-supabase version: 1.0.0 updated: 2026-01-06 --- # Supabase Full-Stack Examples ## Multi-Tenant SaaS Application ### Database Schema ```sql -- Organizations (tenants) CREATE TABLE organizations ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), name TEXT NOT NULL, slug TEXT UNIQUE NOT NULL, plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')), settings JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT NOW() ); -- Organization members with roles CREATE TABLE organization_members ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE, user_id UUID NOT NULL, role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')), joined_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(organization_id, user_id) ); -- Projects within organizations CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE, name TEXT NOT NULL, description TEXT, owner_id UUID NOT NULL, status TEXT DEFAULT 'active', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- Enable RLS ALTER TABLE organizations ENABLE ROW LEVEL SECURITY; ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY; ALTER TABLE projects ENABLE ROW LEVEL SECURITY; -- Policies CREATE POLICY "org_member_select" ON organizations FOR SELECT USING (id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid())); CREATE POLICY "org_admin_update" ON organizations FOR UPDATE USING (id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid() AND role IN ('owner', 'admin'))); CREATE POLICY "member_view" ON organization_members FOR SELECT USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid())); CREATE POLICY "project_access" ON projects FOR ALL USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid())); -- Indexes CREATE INDEX idx_org_members_user ON organization_members(user_id); CREATE INDEX idx_org_members_org ON organization_members(organization_id); CREATE INDEX idx_projects_org ON projects(organization_id); ``` ### TypeScript Service Layer ```typescript // services/organization-service.ts import { supabase } from '@/lib/supabase/client' import type { Database } from '@/types/database' type Organization = Database['public']['Tables']['organizations']['Row'] type OrganizationMember = Database['public']['Tables']['organization_members']['Row'] export class OrganizationService { async create(name: string, slug: string): Promise { const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error('Not authenticated') // Create organization const { data: org, error: orgError } = await supabase .from('organizations') .insert({ name, slug }) .select() .single() if (orgError) throw orgError // Add creator as owner const { error: memberError } = await supabase .from('organization_members') .insert({ organization_id: org.id, user_id: user.id, role: 'owner' }) if (memberError) { // Rollback org creation await supabase.from('organizations').delete().eq('id', org.id) throw memberError } return org } async getMyOrganizations(): Promise { const { data, error } = await supabase .from('organizations') .select('*, organization_members!inner(role)') .order('name') if (error) throw error return data } async getMembers(orgId: string): Promise { const { data, error } = await supabase .from('organization_members') .select('*, user:profiles(*)') .eq('organization_id', orgId) .order('joined_at') if (error) throw error return data } async inviteMember(orgId: string, email: string, role: string): Promise { const { error } = await supabase.functions.invoke('invite-member', { body: { organizationId: orgId, email, role } }) if (error) throw error } } export const organizationService = new OrganizationService() ``` ## AI Document Search Application ### Database Schema ```sql -- Enable extensions CREATE EXTENSION IF NOT EXISTS vector; -- Documents with embeddings CREATE TABLE documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, title TEXT NOT NULL, content TEXT NOT NULL, embedding vector(1536), metadata JSONB DEFAULT '{}', created_by UUID NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); -- HNSW index for fast similarity search CREATE INDEX idx_documents_embedding ON documents USING hnsw (embedding vector_cosine_ops) WITH (m = 16, ef_construction = 64); -- Full-text search index CREATE INDEX idx_documents_content_fts ON documents USING gin(to_tsvector('english', content)); -- Enable RLS ALTER TABLE documents ENABLE ROW LEVEL SECURITY; CREATE POLICY "document_access" ON documents FOR ALL USING (project_id IN ( SELECT p.id FROM projects p JOIN organization_members om ON p.organization_id = om.organization_id WHERE om.user_id = auth.uid() )); -- Semantic search function CREATE OR REPLACE FUNCTION search_documents( p_project_id UUID, p_query_embedding vector(1536), p_match_threshold FLOAT DEFAULT 0.7, p_match_count INT DEFAULT 10 ) RETURNS TABLE ( id UUID, title TEXT, content TEXT, similarity FLOAT ) LANGUAGE plpgsql AS $$ BEGIN RETURN QUERY SELECT d.id, d.title, d.content, 1 - (d.embedding <=> p_query_embedding) AS similarity FROM documents d WHERE d.project_id = p_project_id AND 1 - (d.embedding <=> p_query_embedding) > p_match_threshold ORDER BY d.embedding <=> p_query_embedding LIMIT p_match_count; END; $$; ``` ### Edge Function for Embeddings ```typescript // supabase/functions/generate-embedding/index.ts import { serve } from 'https://deno.land/std@0.168.0/http/server.ts' import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type' } serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }) } try { const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const { documentId, content } = await req.json() // Generate embedding using OpenAI const embeddingResponse = await fetch('https://api.openai.com/v1/embeddings', { method: 'POST', headers: { 'Authorization': `Bearer ${Deno.env.get('OPENAI_API_KEY')}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'text-embedding-ada-002', input: content.slice(0, 8000) }) }) const embeddingData = await embeddingResponse.json() const embedding = embeddingData.data[0].embedding // Update document with embedding const { error } = await supabase .from('documents') .update({ embedding }) .eq('id', documentId) if (error) throw error return new Response( JSON.stringify({ success: true }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } catch (error) { return new Response( JSON.stringify({ error: error.message }), { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } ) } }) ``` ### React Search Component ```typescript // components/DocumentSearch.tsx 'use client' import { useState, useEffect } from 'react' import { useDebounce } from '@/hooks/useDebounce' import { documentService } from '@/services/document-service' interface SearchResult { id: string title: string content: string similarity: number } export function DocumentSearch({ projectId }: { projectId: string }) { const [query, setQuery] = useState('') const [results, setResults] = useState([]) const [loading, setLoading] = useState(false) const debouncedQuery = useDebounce(query, 300) useEffect(() => { if (debouncedQuery.length < 3) { setResults([]) return } async function search() { setLoading(true) try { const data = await documentService.semanticSearch(projectId, debouncedQuery) setResults(data) } catch (error) { console.error('Search failed:', error) } finally { setLoading(false) } } search() }, [debouncedQuery, projectId]) return (
setQuery(e.target.value)} placeholder="Search documents..." className="w-full px-4 py-2 border rounded-lg" /> {loading &&
Searching...
}
{results.map((result) => (

{result.title}

{(result.similarity * 100).toFixed(1)}% match

{result.content}

))}
) } ``` ## Real-Time Collaboration ### Collaborative Editor with Presence ```typescript // components/CollaborativeEditor.tsx 'use client' import { useEffect, useState, useCallback } from 'react' import { supabase } from '@/lib/supabase/client' interface User { id: string name: string color: string } interface PresenceState { user: User cursor: { x: number; y: number } | null selection: { start: number; end: number } | null } export function CollaborativeEditor({ documentId, currentUser }: { documentId: string currentUser: User }) { const [content, setContent] = useState('') const [otherUsers, setOtherUsers] = useState([]) const [channel, setChannel] = useState | null>(null) useEffect(() => { const ch = supabase.channel(`doc:${documentId}`, { config: { presence: { key: currentUser.id } } }) ch.on('presence', { event: 'sync' }, () => { const state = ch.presenceState() const users = Object.values(state) .flat() .filter((p) => p.user.id !== currentUser.id) setOtherUsers(users) }) ch.on('broadcast', { event: 'content-update' }, ({ payload }) => { if (payload.userId !== currentUser.id) { setContent(payload.content) } }) ch.subscribe(async (status) => { if (status === 'SUBSCRIBED') { await ch.track({ user: currentUser, cursor: null, selection: null }) } }) setChannel(ch) return () => { supabase.removeChannel(ch) } }, [documentId, currentUser]) const handleContentChange = useCallback( async (newContent: string) => { setContent(newContent) if (channel) { await channel.send({ type: 'broadcast', event: 'content-update', payload: { userId: currentUser.id, content: newContent } }) } }, [channel, currentUser.id] ) return (