Database
LaunchKit uses Supabase as the database solution, providing you with a full PostgreSQL database with real-time capabilities, built-in authentication, and automatic API generation.
-
Create a Supabase Project: Go to supabase.com and create a new project.
-
Get Your Credentials: In your Supabase dashboard, go to Settings > API to find your:
- Project URL
- Anon/Public Key
- Service Role Key
-
Add Environment Variables: Add your Supabase credentials to
.env.local
:
# -----------------------------------------------------------------------------# Database URI# -----------------------------------------------------------------------------NEXT_PUBLIC_APP_URL=NEXT_PUBLIC_SUPABASE_URL=NEXT_PUBLIC_SUPABASE_ANON_KEY=SUPABASE_SERVICE_ROLE_KEY=
Database Schema
Section titled “Database Schema”LaunchKit comes with pre-defined database tables optimized for SaaS applications with authentication, payments, and lead generation.
Core Tables
Section titled “Core Tables”- profiles - User profiles (extends Supabase auth.users) with Stripe integration
- leads - Email leads for waitlist and marketing
- customers - Stripe customer data (optional, can be merged with profiles)
1. Creating the Profiles Table
Section titled “1. Creating the Profiles Table”In your Supabase SQL Editor, run this query to add a profiles
table (an extension of the authenticated user to store data like Stripe customer_id, subscription access, etc…):
-- Create the profiles table in the public schemaCREATE TABLE public.profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, name TEXT, email TEXT, image TEXT, customer_id TEXT, price_id TEXT, has_access BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'UTC'), updated_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'UTC'));
-- Create a function to update the updated_at timestampCREATE OR REPLACE FUNCTION update_updated_at()RETURNS TRIGGER AS $$BEGIN NEW.updated_at = (now() AT TIME ZONE 'UTC'); RETURN NEW;END;$$ LANGUAGE plpgsql;
-- Create a trigger to automatically update the updated_at columnCREATE TRIGGER profiles_updated_at BEFORE UPDATE ON public.profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Create a function to handle user signupCREATE OR REPLACE FUNCTION handle_new_user()RETURNS TRIGGER AS $$BEGIN INSERT INTO public.profiles (id, name, email, image) VALUES ( NEW.id, COALESCE(NEW.raw_user_meta_data->>'name', NEW.raw_user_meta_data->>'full_name'), NEW.email, NEW.raw_user_meta_data->>'avatar_url' ); RETURN NEW;END;$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Create a trigger to automatically create a profile on signupCREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE FUNCTION handle_new_user();
2. Creating the Leads Table
Section titled “2. Creating the Leads Table”For collecting email leads and waitlist signups:
-- Create the leads tableCREATE TABLE public.leads ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, email TEXT UNIQUE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'UTC'), updated_at TIMESTAMP WITH TIME ZONE DEFAULT (now() AT TIME ZONE 'UTC'));
-- Create trigger for updated_atCREATE TRIGGER leads_updated_at BEFORE UPDATE ON public.leads FOR EACH ROW EXECUTE FUNCTION update_updated_at();
3. Row Level Security (RLS)
Section titled “3. Row Level Security (RLS)”Enable RLS for secure access to your data:
-- Enable RLS on profiles tableALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
-- Policy: Users can view their own profileCREATE POLICY "Users can view own profile" ON public.profiles FOR SELECT USING (auth.uid() = id);
-- Policy: Users can update their own profileCREATE POLICY "Users can update own profile" ON public.profiles FOR UPDATE USING (auth.uid() = id);
-- Policy: Users can insert their own profile (handled by trigger)CREATE POLICY "Users can insert own profile" ON public.profiles FOR INSERT WITH CHECK (auth.uid() = id);
-- Enable RLS on leads tableALTER TABLE public.leads ENABLE ROW LEVEL SECURITY;
-- Policy: Anyone can insert leads (for public signup forms)CREATE POLICY "Anyone can insert leads" ON public.leads FOR INSERT WITH CHECK (true);
-- Policy: Only authenticated users can view leads (optional)CREATE POLICY "Authenticated users can view leads" ON public.leads FOR SELECT USING (auth.role() = 'authenticated');
Supabase Client Configuration
Section titled “Supabase Client Configuration”LaunchKit includes pre-configured Supabase clients for both client-side and server-side operations.
Client-Side Usage
Section titled “Client-Side Usage”import { createBrowserClient } from '@supabase/ssr';
export const createClient = () => createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! );
Server-Side Usage
Section titled “Server-Side Usage”import { createServerClient } from '@supabase/ssr';import { cookies } from 'next/headers';
export const createClient = () => { const cookieStore = cookies();
return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { cookies: { getAll() { return cookieStore.getAll(); }, setAll(cookiesToSet) { try { cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options) ); } catch { // The `setAll` method was called from a Server Component. // This can be ignored if you have middleware refreshing // user sessions. } }, }, } );};
Common Database Operations
Section titled “Common Database Operations”1. User Profile Management
Section titled “1. User Profile Management”// Get user profileexport async function getProfile(userId: string) { const supabase = createClient();
const { data, error } = await supabase .from('profiles') .select('*') .eq('id', userId) .single();
if (error) { console.error('Error fetching profile:', error); return null; }
return data;}
// Update user profileexport async function updateProfile( userId: string, updates: { name?: string; email?: string; image?: string; }) { const supabase = createClient();
const { data, error } = await supabase .from('profiles') .update(updates) .eq('id', userId) .select() .single();
if (error) { console.error('Error updating profile:', error); throw error; }
return data;}
2. Lead Management
Section titled “2. Lead Management”// Add new leadexport async function addLead(email: string) { const supabase = createClient();
const { data, error } = await supabase .from('leads') .insert({ email }) .select() .single();
if (error) { console.error('Error adding lead:', error); throw error; }
return data;}
// Get all leads (admin only)export async function getLeads() { const supabase = createClient();
const { data, error } = await supabase .from('leads') .select('*') .order('created_at', { ascending: false });
if (error) { console.error('Error fetching leads:', error); throw error; }
return data;}
3. Stripe Integration
Section titled “3. Stripe Integration”// Update user subscription statusexport async function updateUserAccess( userId: string, hasAccess: boolean, priceId?: string) { const supabase = createClient();
const { data, error } = await supabase .from('profiles') .update({ has_access: hasAccess, price_id: priceId, }) .eq('id', userId) .select() .single();
if (error) { console.error('Error updating user access:', error); throw error; }
return data;}
// Add Stripe customer ID to user profileexport async function updateStripeCustomer(userId: string, customerId: string) { const supabase = createClient();
const { data, error } = await supabase .from('profiles') .update({ customer_id: customerId }) .eq('id', userId) .select() .single();
if (error) { console.error('Error updating Stripe customer:', error); throw error; }
return data;}
// Remove user access (on subscription cancellation)export async function removeUserAccess(stripeCustomerId: string) { const supabase = createClient();
const { data, error } = await supabase .from('profiles') .update({ has_access: false, price_id: null, }) .eq('customer_id', stripeCustomerId);
if (error) { console.error('Error removing user access:', error); throw error; }
return data;}
Database Types
Section titled “Database Types”LaunchKit includes TypeScript types for your database. Generate them with:
npx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/database.types.ts
pnpx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/database.types.ts
yarn dlx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/database.types.ts
bunx supabase gen types typescript --project-id YOUR_PROJECT_ID > types/database.types.ts
Then use them in your code:
import { Database } from '@/types/database.types';
type Profile = Database['public']['Tables']['profiles']['Row'];type Lead = Database['public']['Tables']['leads']['Row'];
// Example usageconst updateProfile = async (userId: string, updates: Partial<Profile>) => { const { data, error } = await supabase .from('profiles') .update(updates) .eq('id', userId);};
Migrations
Section titled “Migrations”For database changes, use Supabase migrations:
# Create a new migrationnpx supabase migration new add_new_column
# Apply migrationsnpx supabase db push
# Create a new migrationpnpx supabase migration new add_new_column
# Apply migrationspnpx supabase db push
# Create a new migrationyarn dlx supabase migration new add_new_column
# Apply migrationsyarn dlx supabase db push
# Create a new migrationbunx supabase migration new add_new_column
# Apply migrationsbunx supabase db push
Your Supabase database is now ready to power your LaunchKit application with authentication, real-time updates, and type safety!