| layout | title | parent | nav_order |
|---|---|---|---|
default |
Chapter 1: Getting Started with Supabase |
Supabase Tutorial |
1 |
Welcome to Supabase! If you've ever wanted to build modern applications with a complete backend infrastructure without managing servers, databases, or complex configurations, you're in the right place. Supabase provides everything you need to build scalable, secure applications with minimal setup.
Supabase revolutionizes backend development by:
- Complete Backend Stack - Database, auth, storage, real-time, and edge functions
- PostgreSQL Power - Enterprise-grade database with advanced features
- Instant API Generation - Automatic REST and GraphQL APIs from your database
- Real-time Subscriptions - Live data synchronization across clients
- Built-in Security - Authentication, authorization, and data protection
- Developer Experience - Excellent tooling and documentation
- Open Source - Full control and customization capabilities
-
Create Account: Visit supabase.com and sign up
-
Create Project:
- Click "New Project"
- Choose your organization
- Enter project details
- Select region (choose closest to your users)
- Set database password
-
Wait for Setup: Project creation takes 2-3 minutes
# Install Supabase CLI
npm install -g supabase
# Initialize project
supabase init
# Start local Supabase stack
supabase start
# Access local dashboard at http://localhost:54323# Clone Supabase repository
git clone https://github.com/supabase/supabase
cd supabase
# Use Docker Compose for local deployment
docker-compose up -dLet's create a simple task management application:
# Create a new project directory
mkdir supabase-tasks
cd supabase-tasks
# Initialize with your preferred framework
# For React:
npx create-react-app . --template typescript
# For Next.js:
npx create-next-app . --typescript
# For Vue:
npm create vue@latest .# Install Supabase JavaScript client
npm install @supabase/supabase-js
# For React hooks (optional)
npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'your-project-url'
const supabaseKey = 'your-anon-key'
export const supabase = createClient(supabaseUrl, supabaseKey)
// Types for better TypeScript support
export type Database = {
public: {
Tables: {
tasks: {
Row: {
id: number
title: string
description: string | null
completed: boolean
created_at: string
user_id: string
}
Insert: {
title: string
description?: string | null
completed?: boolean
user_id: string
}
Update: {
title?: string
description?: string | null
completed?: boolean
}
}
}
}
}-- Run this in Supabase SQL Editor
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE
);
-- Enable Row Level Security
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Create policy for users to see only their own tasks
CREATE POLICY "Users can view own tasks" ON tasks
FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own tasks" ON tasks
FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own tasks" ON tasks
FOR UPDATE USING (auth.uid() = user_id);
CREATE POLICY "Users can delete own tasks" ON tasks
FOR DELETE USING (auth.uid() = user_id);// src/hooks/useTasks.ts
import { useState, useEffect } from 'react'
import { supabase } from '../lib/supabase'
import type { Database } from '../lib/supabase'
type Task = Database['public']['Tables']['tasks']['Row']
type TaskInsert = Database['public']['Tables']['tasks']['Insert']
export function useTasks() {
const [tasks, setTasks] = useState<Task[]>([])
const [loading, setLoading] = useState(true)
// Fetch tasks
useEffect(() => {
fetchTasks()
}, [])
async function fetchTasks() {
try {
const { data, error } = await supabase
.from('tasks')
.select('*')
.order('created_at', { ascending: false })
if (error) throw error
setTasks(data || [])
} catch (error) {
console.error('Error fetching tasks:', error)
} finally {
setLoading(false)
}
}
// Add task
async function addTask(task: TaskInsert) {
try {
const { data, error } = await supabase
.from('tasks')
.insert([task])
.select()
.single()
if (error) throw error
setTasks(prev => [data, ...prev])
return data
} catch (error) {
console.error('Error adding task:', error)
throw error
}
}
// Update task
async function updateTask(id: number, updates: Partial<Task>) {
try {
const { data, error } = await supabase
.from('tasks')
.update(updates)
.eq('id', id)
.select()
.single()
if (error) throw error
setTasks(prev => prev.map(task =>
task.id === id ? data : task
))
return data
} catch (error) {
console.error('Error updating task:', error)
throw error
}
}
// Delete task
async function deleteTask(id: number) {
try {
const { error } = await supabase
.from('tasks')
.delete()
.eq('id', id)
if (error) throw error
setTasks(prev => prev.filter(task => task.id !== id))
} catch (error) {
console.error('Error deleting task:', error)
throw error
}
}
return {
tasks,
loading,
addTask,
updateTask,
deleteTask,
refetch: fetchTasks
}
}// src/components/TaskList.tsx
import React, { useState } from 'react'
import { useTasks } from '../hooks/useTasks'
import { supabase } from '../lib/supabase'
export function TaskList() {
const { tasks, loading, addTask, updateTask, deleteTask } = useTasks()
const [newTaskTitle, setNewTaskTitle] = useState('')
const [user, setUser] = useState(null)
// Get current user
React.useEffect(() => {
const getUser = async () => {
const { data: { user } } = await supabase.auth.getUser()
setUser(user)
}
getUser()
}, [])
const handleAddTask = async (e: React.FormEvent) => {
e.preventDefault()
if (!newTaskTitle.trim() || !user) return
try {
await addTask({
title: newTaskTitle,
user_id: user.id
})
setNewTaskTitle('')
} catch (error) {
console.error('Error adding task:', error)
}
}
const handleToggleComplete = async (taskId: number, completed: boolean) => {
try {
await updateTask(taskId, { completed: !completed })
} catch (error) {
console.error('Error updating task:', error)
}
}
const handleDeleteTask = async (taskId: number) => {
try {
await deleteTask(taskId)
} catch (error) {
console.error('Error deleting task:', error)
}
}
if (loading) return <div>Loading tasks...</div>
return (
<div className="task-list">
<h1>My Tasks</h1>
{/* Add new task form */}
<form onSubmit={handleAddTask}>
<input
type="text"
value={newTaskTitle}
onChange={(e) => setNewTaskTitle(e.target.value)}
placeholder="Add a new task..."
required
/>
<button type="submit">Add Task</button>
</form>
{/* Task list */}
<div className="tasks">
{tasks.map(task => (
<div key={task.id} className={`task ${task.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={task.completed}
onChange={() => handleToggleComplete(task.id, task.completed)}
/>
<span className="task-title">{task.title}</span>
<button
onClick={() => handleDeleteTask(task.id)}
className="delete-btn"
>
Delete
</button>
</div>
))}
</div>
</div>
)
}// src/components/Auth.tsx
import React, { useState } from 'react'
import { supabase } from '../lib/supabase'
export function Auth() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const { error } = await supabase.auth.signUp({
email,
password
})
if (error) throw error
alert('Check your email for the confirmation link!')
} catch (error) {
alert(error.message)
} finally {
setLoading(false)
}
}
const handleSignIn = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) throw error
} catch (error) {
alert(error.message)
} finally {
setLoading(false)
}
}
return (
<div className="auth-container">
<form onSubmit={handleSignIn}>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Loading...' : 'Sign In'}
</button>
<button
type="button"
onClick={handleSignUp}
disabled={loading}
>
Sign Up
</button>
</form>
</div>
)
}Supabase Platform
├── Database (PostgreSQL)
│ ├── Tables & Schemas
│ ├── Row Level Security (RLS)
│ ├── Triggers & Functions
│ └── Real-time Subscriptions
├── Authentication
│ ├── User Management
│ ├── OAuth Providers
│ ├── JWT Tokens
│ └── Custom Auth Flows
├── Storage
│ ├── File Uploads
│ ├── CDN Integration
│ ├── Access Policies
│ └── Image Transformations
├── Edge Functions
│ ├── Serverless Functions
│ ├── API Routes
│ ├── Webhooks
│ └── Background Jobs
└── Dashboard & APIs
├── Admin Interface
├── REST API
├── GraphQL API
└── Realtime API
Supabase automatically generates APIs for your database:
// REST API endpoints are automatically created
// GET /rest/v1/tasks - Get all tasks
// POST /rest/v1/tasks - Create new task
// GET /rest/v1/tasks?id=eq.1 - Get specific task
// PATCH /rest/v1/tasks?id=eq.1 - Update task
// DELETE /rest/v1/tasks?id=eq.1 - Delete task
// Client-side usage
const { data, error } = await supabase
.from('tasks')
.select('*')
.eq('completed', false)
.order('created_at', { ascending: false })// Subscribe to real-time changes
const subscription = supabase
.channel('tasks')
.on('postgres_changes',
{
event: '*',
schema: 'public',
table: 'tasks'
},
(payload) => {
console.log('Change received!', payload)
// Update UI with new data
}
)
.subscribe()
// Clean up subscription
subscription.unsubscribe()-- Enable RLS on tables
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
-- Create policies for data access
CREATE POLICY "Users can view own tasks"
ON tasks FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own tasks"
ON tasks FOR INSERT
WITH CHECK (auth.uid() = user_id);# .env.local
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key# Start local Supabase
supabase start
# Reset database
supabase db reset
# Generate types
supabase gen types typescript --local > types/supabase.ts
# View logs
supabase logs# Create migration
supabase migration new create_tasks_table
# Apply migrations
supabase db push
# View migration status
supabase db diff// Test database connection
const testConnection = async () => {
try {
const { data, error } = await supabase
.from('tasks')
.select('count')
.single()
if (error) throw error
console.log('✅ Database connection successful!')
} catch (error) {
console.error('❌ Database connection failed:', error)
}
}// Test authentication
const testAuth = async () => {
try {
const { data, error } = await supabase.auth.getUser()
if (error) throw error
console.log('✅ Authentication working!')
console.log('User:', data.user?.email)
} catch (error) {
console.error('❌ Authentication failed:', error)
}
}Congratulations! 🎉 You've successfully:
- Set up a Supabase project with database and authentication
- Created a complete task management application with full CRUD operations
- Implemented user authentication with secure access controls
- Built real-time features for live data synchronization
- Configured Row Level Security for data protection
- Used automatic API generation for seamless data access
- Set up development workflow with local environment
- Implemented security best practices for production readiness
Now that you have a working Supabase application, let's dive deeper into database design and management. In Chapter 2: Database Design & Management, we'll explore advanced PostgreSQL features, migrations, and data modeling strategies.
Practice what you've learned:
- Add more features to your task app (due dates, categories, priorities)
- Implement real-time updates for multiple users
- Add file attachments using Supabase Storage
- Create a mobile version using React Native
What kind of application are you most excited to build with Supabase's powerful features? 🚀
Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for error, tasks, supabase so behavior stays predictable as complexity grows.
In practical terms, this chapter helps you avoid three common failures:
- coupling core logic too tightly to one implementation path
- missing the handoff boundaries between setup, execution, and validation
- shipping changes without clear rollback or observability strategy
After working through this chapter, you should be able to reason about Chapter 1: Getting Started with Supabase as an operating subsystem inside Supabase Tutorial: Building Modern Backend Applications, with explicit contracts for inputs, state transitions, and outputs.
Use the implementation notes around task, auth, console as your checklist when adapting these patterns to your own repository.
Under the hood, Chapter 1: Getting Started with Supabase usually follows a repeatable control path:
- Context bootstrap: initialize runtime config and prerequisites for
error. - Input normalization: shape incoming data so
tasksreceives stable contracts. - Core execution: run the main logic branch and propagate intermediate state through
supabase. - Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
- Output composition: return canonical result payloads for downstream consumers.
- Operational telemetry: emit logs/metrics needed for debugging and performance tuning.
When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.
Use the following upstream sources to verify implementation details while reading this chapter:
- View Repo
Why it matters: authoritative reference on
View Repo(github.com).
Suggested trace strategy:
- search upstream code for
errorandtasksto map concrete implementation paths - compare docs claims against actual runtime/config code before reusing patterns in production