Supabase: The Backend for Vibe Coders
Overview
So, you want to build something cool but the backend stuff feels like a total drag. Databases, authentication, servers... it's a lot. What if you could get all that sorted out with a few clicks and a bit of code?
Enter Supabase. It's an open-source platform that gives you a full backend – a database, user authentication, file storage, and more – without you having to be a backend genius. It’s perfect for vibe coding because it lets you focus on what actually matters: building your app and getting your ideas out there, fast.
This tutorial will give you the lowdown on the essential Supabase features. We'll get you set up, teach you how to save and retrieve data, handle user sign-ups, and even store files.
Let's get into the vibe 🤗
Step 1: Get Your Supabase Project Started
First things first, you need a Supabase account and a project.
- Go to supabase.com and sign up. You can use your GitHub account to make it super quick.
- Once you're in, create a new organization, and create a "New Project".
- Give your project a name, generate a secure database password (and save it somewhere safe like a password manager!), and choose a region close to you.
- Your backend is being built!
When it's ready, navigate to your project dashboard -> Project Settings. We'll get some important environment variables for your app:
- Find your Project URL under Data API section.
- Find your Publishable key and Secret key in API Keys section.
Keep this tab open. You'll need to copy these keys into your app soon.
Step 2: Create the todos table
Use the Table Editor or run the SQL below.
create table if not exists public.todos (
id bigint generated by default as identity primary key,
user_id uuid not null references auth.users (id) on delete cascade,
title text not null,
is_complete boolean not null default false,
inserted_at timestamp with time zone default now()
);
alter table public.todos enable row level security;
Step 3: Initialize Supabase in your app
Install the client:
npm install @supabase/supabase-js
Create a client file for the browser (safe with RLS + policies):
// lib/supabaseClient.js import { createClient } from '@supabase/supabase-js' const supabaseUrl = 'YOUR_SUPABASE_PROJECT_URL' const supabaseKey = 'YOUR_SUPABASE_PUBLISHABLE_KEY' // formerly "anon" key export const supabase = createClient(supabaseUrl, supabaseKey) // Tip: prefer env vars (Astro/Next/etc.) // const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL // const supabaseKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY
If you also have server code (API routes, CRON, webhooks), use the Secret key there only:
// server/supabaseServer.js (Node/Edge runtime) import { createClient } from '@supabase/supabase-js' export const supabaseServer = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_SECRET_KEY // never expose to the client )
Step 4: Auth — Sign up and sign in
// Sign up async function signUpUser(email, password) { const { data, error } = await supabase.auth.signUp({ email, password }) if (error) { console.error('Error signing up:', error.message); return null } return data.user } // Sign in async function signInUser(email, password) { const { data, error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { console.error('Error signing in:', error.message); return null } return data.user }
Step 5: CRUD — Add, list, toggle, and delete todos
// Add a todo (requires authenticated user) async function addTodo(title) { const { data: { user } } = await supabase.auth.getUser() if (!user) throw new Error('Not signed in') const { data, error } = await supabase .from('todos') .insert([{ title, user_id: user.id }]) .select() if (error) { console.error('Error adding todo:', error.message); return null } return data } // List todos (most recent first) async function fetchTodos() { const { data, error } = await supabase .from('todos') .select('*') .order('inserted_at', { ascending: false }) if (error) { console.error('Error fetching todos:', error.message); return [] } return data } // Toggle completion async function toggleTodo(todoId, nextComplete) { const { data, error } = await supabase .from('todos') .update({ is_complete: nextComplete }) .eq('id', todoId) .select() if (error) { console.error('Error updating todo:', error.message); return null } return data } // Delete a todo async function deleteTodo(todoId) { const { error } = await supabase .from('todos') .delete() .eq('id', todoId) if (error) { console.error('Error deleting todo:', error.message) } }
Step 6: Secure with RLS (policies)
Your Publishable key is client-safe only when Row Level Security (RLS) is enabled and policies are correct. Lock the table to row owners:
create policy "Select own todos" on public.todos
for select using (auth.uid() = user_id);
create policy "Insert own todos" on public.todos
for insert with check (auth.uid() = user_id);
create policy "Update own todos" on public.todos
for update using (auth.uid() = user_id);
create policy "Delete own todos" on public.todos
for delete using (auth.uid() = user_id);
Optional: Realtime updates
const { data: { user } } = await supabase.auth.getUser() const channel = supabase .channel('todos-changes') .on('postgres_changes', { event: '*', schema: 'public', table: 'todos', filter: `user_id=eq.${user.id}` }, payload => { console.log('Change received!', payload) // e.g., re-fetch or update local state }) .subscribe()
Wrap-up
You built a secure Todo backend with Supabase:
- Created a
todos
table - Wired up client with the Publishable key
- Implemented auth and CRUD
- Secured data with RLS policies
- (Optional) Realtime updates
Now plug this into your UI framework of choice and ship it. Vibe on.