Beginner's Guide: Connect React with Supabase 🚀
If you already know basic React (components, useState, useEffect), this guide will show you how to connect your React app to Supabase — an open-source Firebase alternative — and build a simple To-Do app with full CRUD (Create, Read, Update, Delete).
Let's go step by step. No prior Supabase knowledge needed.
What is Supabase?
Supabase gives you a Postgres database, authentication, and instant APIs — without writing any backend code. Think of it as a backend-as-a-service.
For this guide, we'll just use the database + auto-generated API part.
Step 1: Create a Supabase Project
- Go to supabase.com and sign up (GitHub login is fastest).
- Click New Project.
- Fill in:
-
Name:
todo-app(anything you like) - Database Password: save this somewhere safe
- Region: pick the closest one to you
-
Name:
- Click Create new project and wait ~1-2 minutes while Supabase sets everything up.
Step 2: Create the todos Table
- In your Supabase project dashboard, go to the Table Editor (left sidebar).
- Click New Table.
- Name it
todos. - Add these columns (in addition to the default
idandcreated_at):
| Column Name | Type | Default |
|---|---|---|
task |
text |
— |
is_complete |
bool |
false |
- Click Save.
💡 Tip: You can also do this via the SQL Editor by running:
create table todos ( id bigint generated by default as identity primary key, task text not null, is_complete boolean default false, created_at timestamp with time zone default now() );
Turn off Row Level Security (for learning purposes only)
Go to Authentication > Policies (or Table Editor > todos > RLS), and disable RLS for now so your students can read/write freely without setting up auth.
⚠️ Important for your session: Tell your juniors this is only for a demo/learning project. In a real production app, RLS should always be enabled with proper policies.
Step 3: Get Your API Keys
- Go to Project Settings > API.
- Copy two things:
-
Project URL (looks like
https://xxxxx.supabase.co) - anon public key (a long string)
-
Project URL (looks like
You'll need both in your React app.
Step 4: Set Up the React Project
If they already have a React project (Vite recommended), just install the Supabase client:
npm install @supabase/supabase-js
If starting fresh:
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm install @supabase/supabase-js
Step 5: Create a Supabase Client File
Create a file src/supabaseClient.js:
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'YOUR_PROJECT_URL'
const supabaseAnonKey = 'YOUR_ANON_KEY'
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
💡 Best practice: use environment variables instead of hardcoding keys.
Create a.envfile:VITE_SUPABASE_URL=your_project_url VITE_SUPABASE_ANON_KEY=your_anon_keyThen use:
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY(For a beginner live demo, hardcoding is fine to keep things simple — just mention the
.envbest practice.)
Step 6: Build the To-Do Component
Replace src/App.jsx with this:
import { useState, useEffect } from 'react'
import { supabase } from './supabaseClient'
import './App.css'
function App() {
const [todos, setTodos] = useState([])
const [newTask, setNewTask] = useState('')
const [loading, setLoading] = useState(true)
// Fetch todos when component loads
useEffect(() => {
fetchTodos()
}, [])
async function fetchTodos() {
setLoading(true)
const { data, error } = await supabase
.from('todos')
.select('*')
.order('created_at', { ascending: false })
if (error) console.error('Error fetching todos:', error)
else setTodos(data)
setLoading(false)
}
async function addTodo(e) {
e.preventDefault()
if (!newTask.trim()) return
const { data, error } = await supabase
.from('todos')
.insert([{ task: newTask, is_complete: false }])
.select()
if (error) console.error('Error adding todo:', error)
else {
setTodos([data[0], ...todos])
setNewTask('')
}
}
async function toggleComplete(id, currentStatus) {
const { error } = await supabase
.from('todos')
.update({ is_complete: !currentStatus })
.eq('id', id)
if (error) console.error('Error updating todo:', error)
else {
setTodos(todos.map(t => t.id === id ? { ...t, is_complete: !currentStatus } : t))
}
}
async function deleteTodo(id) {
const { error } = await supabase
.from('todos')
.delete()
.eq('id', id)
if (error) console.error('Error deleting todo:', error)
else setTodos(todos.filter(t => t.id !== id))
}
return (
<div style={{ maxWidth: 500, margin: '40px auto', fontFamily: 'sans-serif' }}>
<h1>📝 Supabase To-Do App</h1>
<form onSubmit={addTodo} style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
<input
type="text"
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
placeholder="Add a new task..."
style={{ flex: 1, padding: 8 }}
/>
<button type="submit" style={{ padding: '8px 16px' }}>Add</button>
</form>
{loading ? (
<p>Loading...</p>
) : (
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map((todo) => (
<li
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
gap: 10,
padding: 10,
borderBottom: '1px solid #eee',
}}
>
<input
type="checkbox"
checked={todo.is_complete}
onChange={() => toggleComplete(todo.id, todo.is_complete)}
/>
<span
style={{
flex: 1,
textDecoration: todo.is_complete ? 'line-through' : 'none',
color: todo.is_complete ? '#999' : '#000',
}}
>
{todo.task}
</span>
<button onClick={() => deleteTodo(todo.id)} style={{ color: 'red' }}>
Delete
</button>
</li>
))}
</ul>
)}
</div>
)
}
export default App
Step 7: Run It
npm run dev
Open the local URL shown in the terminal — you should be able to add, complete, and delete tasks, and see them saved in your Supabase Table Editor in real time.
Top comments (1)
The line that hit me: "Turn off Row Level Security (for learning purposes only)" — followed immediately by the warning to turn it back on.
I'm a beginner — three weeks into Python, 23 articles published. I've never touched React or Supabase. But I built the exact same project you're describing, just in a different stack.
Mine was a Python CLI to-do app that saves to a JSON file. Yours is a React app connected to Supabase. Same features — add, complete, delete — but the architecture is completely different. And reading yours, I understood why people reach for a real backend.
My version saves to a local file. Yours saves to a database that any device can reach. My version breaks if you move the file. Yours doesn't. My version can't be accessed from a phone. Yours can.
The part I appreciated most was the honesty about RLS. Most tutorials skip that warning entirely, and then beginners deploy something insecure without knowing it. You flagged it clearly and explained why it matters. That's the kind of teaching I try to do in my own writing.
Great guide. Saving it for when I eventually learn JavaScript.