Skip to content
Learni
View all tutorials
Base de données

How to Get Started with Supabase in 2026

Lire en français

Introduction

Supabase is an open-source platform that combines a PostgreSQL database, authentication, storage, and realtime subscriptions, all with ready-to-use REST and GraphQL APIs. Unlike Firebase, Supabase uses standard SQL, making it more powerful for complex relationships and migrations.

Why use it in 2026? Modern web apps demand scalability, Row Level Security (RLS), and seamless integration with Next.js or React. This beginner tutorial takes you from zero: project creation, auth, CRUD on tables, and realtime. At the end, you'll have a working todo list app connected to Supabase.

Key advantage: intuitive dashboard for no-code setup, then JS client for integration. Save weeks of backend dev time.

Prerequisites

  • A free account on supabase.com
  • Node.js 18+ installed
  • Basic HTML/JS knowledge (no framework required)
  • A code editor like VS Code
  • Modern browser for testing

Step 1: Create Your Supabase Project

Log in to app.supabase.com and create a new project. Choose a name, a nearby region (e.g., Europe West), and a strong DB password. Wait about 2 minutes for initialization.

Note your project URL and anon key (public) from Settings > API. These are essential for the JS client. Enable email authentication in Authentication > Settings > Enable Email.

Install the Supabase JS Client

terminal
mkdir supabase-todo-app
cd supabase-todo-app
npm init -y
npm install @supabase/supabase-js

This command initializes a Node project and installs the official Supabase JS client, compatible with browsers and servers. Use npm run dev later for a local server if needed. Always use the latest version (^2.x in 2026) and avoid outdated ones.

Set Up the Supabase Client

supabase.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = 'https://your-project.supabase.co'
const supabaseAnonKey = 'your-anon-key-here'

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Replace your-project and your-anon-key with your dashboard values. This singleton client handles all connections. Use the anon key for frontend (safe to expose publicly); service_role for server-side admin. Pitfall: Never commit keys to Git.

Step 2: Create a Todos Table

In the Supabase dashboard > Table Editor, create a todos table with these columns:

  • id (uuid, primary key, default gen_random_uuid())
  • task (text, not null)
  • is_complete (boolean, default false)
  • user_id (uuid, references auth.users(id))

Enable RLS: ALTER TABLE todos ENABLE ROW LEVEL SECURITY;. Add a policy: CREATE POLICY user_todos ON todos FOR ALL USING (auth.uid() = user_id); via SQL Editor for per-user security.

Create the Table via SQL

create-table.sql
CREATE TABLE todos (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  task TEXT NOT NULL,
  is_complete BOOLEAN DEFAULT false,
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE
);

ALTER TABLE todos ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Users can manage own todos" ON todos
  FOR ALL
  USING (auth.uid() = user_id)
  WITH CHECK (auth.uid() = user_id);

Run this full SQL in the dashboard's SQL Editor. RLS protects data: only the owner accesses their todos. Think of it like a per-user safe. Pitfall: Forget ON DELETE CASCADE and orphans clutter the DB.

Sign Up and Log In

auth.js
import { supabase } from './supabase.js'

export async function signUp(email, password) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  })
  if (error) throw error
  return data
}

export async function signIn(email, password) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  })
  if (error) throw error
  return data
}

export async function signOut() {
  const { error } = await supabase.auth.signOut()
  if (error) throw error
}

These functions handle email/password auth. signUp sends a confirmation email (enable in dashboard). Always check error. In production, add CAPTCHA via settings. Pitfall: Passwords under 6 chars fail silently.

Step 3: Manage Todos (CRUD)

Create: Add a todo linked to the logged-in user.
Read: Fetch the user's todos.
Update/Delete: Modify/delete by ID.
Use supabase.from('todos').select('*') for simple queries.

CRUD Functions for Todos

todos.js
import { supabase } from './supabase.js'

const userId = supabase.auth.getUser().data.user.id // Call after login

export async function addTodo(task) {
  const { data, error } = await supabase
    .from('todos')
    .insert([{ task, user_id: userId }])
  if (error) throw error
  return data
}

export async function getTodos() {
  const { data, error } = await supabase
    .from('todos')
    .select('*')
    .eq('user_id', userId)
  if (error) throw error
  return data
}

export async function toggleTodo(id, isComplete) {
  const { error } = await supabase
    .from('todos')
    .update({ is_complete: isComplete })
    .eq('id', id)
  if (error) throw error
}

export async function deleteTodo(id) {
  const { error } = await supabase
    .from('todos')
    .delete()
    .eq('id', id)
  if (error) throw error
}

Complete CRUD code with built-in RLS. .eq('user_id', userId) auto-filters via policy. userId comes from auth session. Test with console.log. Pitfall: Fetching userId before login returns null and crashes.

Realtime Subscriptions

realtime.js
import { supabase } from './supabase.js'

function subscribeToTodos(callback) {
  return supabase
    .channel('todos')
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'todos',
        filter: `user_id=eq.${supabase.auth.getUser().data.user.id}`,
      },
      callback
    )
    .subscribe()
}

export { subscribeToTodos }

Subscribe to real-time DB changes. '*' listens for insert/update/delete. Filter by user_id for performance. Callback receives the payload. Unsubscribe with .unsubscribe(). Pitfall: No filter causes overload on high traffic.

Complete HTML/JS Example

index.html
<!DOCTYPE html>
<html>
<head><title>Supabase Todos</title></head>
<body>
  <input id="task" placeholder="Nouvelle tâche">
  <button onclick="addTodo()">Ajouter</button>
  <button onclick="signIn()">Login</button>
  <ul id="todos"></ul>
  <script type="module">
    import { supabase } from './supabase.js'
    import { addTodo, getTodos, toggleTodo, deleteTodo, signIn, signUp } from './todos.js'
    import { subscribeToTodos } from './realtime.js'

    let todos = []

    async function loadTodos() {
      todos = await getTodos()
      render()
    }

    function render() {
      const ul = document.getElementById('todos')
      ul.innerHTML = todos.map(t => `
        <li>
          <span style="text-decoration: ${t.is_complete ? 'line-through' : 'none'}">${t.task}</span>
          <button onclick="toggleTodo('${t.id}')">Toggle</button>
          <button onclick="deleteTodo('${t.id}')">Suppr</button>
        </li>
      `).join('')
    }

    window.addTodo = addTodo
    window.getTodos = loadTodos
    window.toggleTodo = async(id) => {
      const todo = todos.find(t => t.id === id)
      await toggleTodo(id, !todo.is_complete)
      loadTodos()
    }
    window.deleteTodo = async(id) => {
      await deleteTodo(id)
      loadTodos()
    }
    window.signIn = () => { /* Implement real login UI */ alert('Login simulated') }

    subscribeToTodos(() => loadTodos())
    loadTodos()
  </script>
</body>
</html>

Standalone HTML page: type a task, add it, toggle in realtime. Uses ES modules. Replace signIn with a real form. Serve with npx serve .. Pitfall: CORS – enable in Supabase dashboard > Auth > URL config.

Best Practices

  • Always use RLS: Blocks unauthorized access even if queries are poorly filtered.
  • Separate keys: Anon for frontend, service_role for server-only via env vars.
  • Optimized queries: Index filtered columns (e.g., CREATE INDEX ON todos(user_id);).
  • Error handling: Wrap everything in try/catch, show user-friendly toasts.
  • SQL migrations: Version schemas via Supabase GitHub repo.

Common Errors to Avoid

  • Forget email confirmation: Users stuck in 'confirmed=false' state.
  • No user_id filter: RLS bypass if policy is wrong, data leak.
  • Client without await: Promises rejected silently, broken UI.
  • Ignore realtime cleanup: Subscriptions leak memory on multi-tabs.

Next Steps

  • Official docs: supabase.com/docs
  • Integrate with Next.js: Edge Functions for serverless.
  • Advanced: Storage for files, Vectors for AI.
  • Check out our Learni backend courses to master Supabase professionally.