Build a Full-Stack App with React and Supabase
    React
    Supabase
    TypeScript
    Full Stack
    Tutorial

    Build a Full-Stack App with React and Supabase

    A complete tutorial on building a full-stack React and TypeScript application with Supabase — covering database setup, Row Level Security, authentication, React Query data fetching, real-time subscriptions, and Vercel deployment.

    HOA

    Harrison Onyango Aloo

    Software Engineer & Full Stack Developer

    June 17, 2026
    5 min read

    Build a Full-Stack App with React and Supabase

    Supabase is an open-source Firebase alternative that gives you a PostgreSQL database, authentication, real-time subscriptions, and file storage — all with a generous free tier. Paired with React, it lets you ship production-grade full-stack apps without managing a backend server.

    In this tutorial we'll build a task management app from scratch: React + TypeScript on the frontend, Supabase as the entire backend.

    What We're Building

    • Email and password authentication
    • A tasks table with per-user Row Level Security
    • Real-time updates across browser tabs
    • Deployed to Vercel in minutes

    Prerequisites

    • Node.js 18+
    • A free Supabase account at supabase.com
    • Basic knowledge of React and TypeScript

    Step 1: Create a Supabase Project

    1. Go to supabase.com and create a new project
    2. Copy your Project URL and anon public key from Settings → API
    3. Open the SQL Editor and run:
    CREATE TABLE tasks (
        id         UUID DEFAULT gen_random_uuid() PRIMARY KEY,
        user_id    UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
        title      TEXT NOT NULL,
        completed  BOOLEAN DEFAULT FALSE,
        created_at TIMESTAMPTZ DEFAULT NOW()
    );
    
    ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
    
    CREATE POLICY "Users manage own tasks"
    ON tasks FOR ALL
    USING (auth.uid() = user_id)
    WITH CHECK (auth.uid() = user_id);
    

    Row Level Security means even if someone gets your anon key, they can only access their own rows — enforced at the database level, not in your app code.

    Step 2: Set Up React

    npm create vite@latest my-app -- --template react-ts
    cd my-app
    npm install @supabase/supabase-js @tanstack/react-query
    

    Create .env:

    VITE_SUPABASE_URL=https://your-project.supabase.co
    VITE_SUPABASE_ANON_KEY=your-anon-key
    

    Create src/lib/supabase.ts:

    import { createClient } from '@supabase/supabase-js';
    
    export const supabase = createClient(
      import.meta.env.VITE_SUPABASE_URL,
      import.meta.env.VITE_SUPABASE_ANON_KEY
    );
    

    Step 3: Authentication

    Supabase handles the full auth flow — sign up, sign in, sessions, and token refresh automatically:

    // Sign up
    const { error } = await supabase.auth.signUp({ email, password });
    
    // Sign in
    const { error } = await supabase.auth.signInWithPassword({ email, password });
    
    // Sign out
    await supabase.auth.signOut();
    
    // Get current user
    const { data: { user } } = await supabase.auth.getUser();
    
    // Listen to auth changes
    supabase.auth.onAuthStateChange((event, session) => {
      if (event === 'SIGNED_IN') setUser(session?.user ?? null);
      if (event === 'SIGNED_OUT') setUser(null);
    });
    

    Step 4: Fetch Data with React Query

    Create src/hooks/useTasks.ts:

    import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
    import { supabase } from '../lib/supabase';
    
    export function useTasks() {
      return useQuery({
        queryKey: ['tasks'],
        queryFn: async () => {
          const { data, error } = await supabase
            .from('tasks')
            .select('*')
            .order('created_at', { ascending: false });
    
          if (error) throw error;
          return data;
        },
      });
    }
    
    export function useAddTask() {
      const queryClient = useQueryClient();
    
      return useMutation({
        mutationFn: async (title: string) => {
          const { data: { user } } = await supabase.auth.getUser();
    
          const { error } = await supabase
            .from('tasks')
            .insert({ title, user_id: user!.id });
    
          if (error) throw error;
        },
        onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }),
      });
    }
    
    export function useToggleTask() {
      const queryClient = useQueryClient();
    
      return useMutation({
        mutationFn: async ({ id, completed }: { id: string; completed: boolean }) => {
          const { error } = await supabase
            .from('tasks')
            .update({ completed })
            .eq('id', id);
    
          if (error) throw error;
        },
        onSuccess: () => queryClient.invalidateQueries({ queryKey: ['tasks'] }),
      });
    }
    

    Step 5: Real-Time Updates

    Supabase can push database changes to your UI the moment they happen — across all open tabs:

    import { useEffect } from 'react';
    import { useQueryClient } from '@tanstack/react-query';
    import { supabase } from '../lib/supabase';
    
    export function useRealtimeTasks() {
      const queryClient = useQueryClient();
    
      useEffect(() => {
        const channel = supabase
          .channel('tasks-changes')
          .on(
            'postgres_changes',
            { event: '*', schema: 'public', table: 'tasks' },
            () => {
              queryClient.invalidateQueries({ queryKey: ['tasks'] });
            }
          )
          .subscribe();
    
        return () => { supabase.removeChannel(channel); };
      }, [queryClient]);
    }
    

    Call useRealtimeTasks() in your tasks component. Every insert, update, or delete now instantly refreshes the UI.

    Step 6: Deploy to Vercel

    npm install -g vercel
    vercel
    

    Add your environment variables in the Vercel dashboard under Settings → Environment Variables:

    • VITE_SUPABASE_URL
    • VITE_SUPABASE_ANON_KEY

    Redeploy and your full-stack app is live.

    Supabase vs Firebase

    FeatureSupabaseFirebase
    DatabasePostgreSQL (relational)Firestore (NoSQL)
    Open sourceYesNo
    Self-hostableYesNo
    Free tier500MB, 2 projects1GB Firestore
    Real-timeYesYes
    TypeScript typesAuto-generatedManual

    If you know SQL and need relational data, Supabase wins every time.

    Conclusion

    Supabase eliminates the need to build and maintain an API server. Row Level Security handles authorization at the database level, the JS client handles auth, and React Query manages server state cleanly. Together they cover everything needed to ship a production app fast.

    This is the exact stack powering this portfolio — the blog posts, projects, and experience sections you see here are all live data from Supabase.

    HOA

    Harrison Onyango Aloo

    Software Engineer & Full Stack Developer

    Thanks for reading — if this article helped you, here's how you can support the work. It helps me keep writing in-depth technical content and maintaining my open-source projects.

    HOA

    Harrison Aloo

    Software Engineer & Full Stack Developer building digital solutions that connect, scale, and inspire.

    Connect

    © 2026 Harrison Onyango Aloo. All rights reserved.

    Chat on WhatsApp