Switch Language
Toggle Theme

Supabase Getting Started: PostgreSQL + Auth + Storage All-in-One Backend

It was 3 AM, and I was staring at the 12th database connection error on my screen. That’s when it hit me: for frontend developers, building a full-stack project is genuinely challenging.

Let’s be honest—every time I needed backend functionality, I had to learn Node.js, Express, figure out database configuration, user authentication, file storage… each one was a rabbit hole. Until I discovered Supabase.

Simply put, it’s an open-source Firebase alternative. But here’s the kicker: it uses PostgreSQL, not the headache-inducing NoSQL. And it packages database, user authentication, and file storage into one neat solution—an all-in-one backend service.

This article will guide you through Supabase from scratch, focusing on three core features: Database, Auth, and Storage. By the end, you’ll be able to quickly spin up a complete backend without wrestling with configuration files.


What is Supabase

Let’s start with what Supabase actually is.

It’s a BaaS platform—Backend as a Service. What does that mean? You don’t need to set up servers, configure databases, or write APIs. It handles all of that for you.

Unlike Firebase, Supabase is completely open source. And importantly, it uses PostgreSQL. This matters because PostgreSQL is a relational database—it supports complex SQL queries and has clear data relationships, which is much more practical than Firestore’s document-based approach.

Supabase has six core features:

  • Database: PostgreSQL database with SQL query support and auto-generated REST API
  • Auth: User authentication system supporting email login, social login (Google, GitHub, etc.), and JWT tokens
  • Storage: File storage similar to AWS S3, but simpler
  • Realtime: Real-time data synchronization, perfect for chat applications
  • Edge Functions: Edge computing, similar to AWS Lambda
  • Vector Database: Vector database for AI applications

However, this article focuses on the first three: Database, Auth, and Storage—these are the core features. We’ll cover the others later.

At this point, you might wonder: it’s open source, uses PostgreSQL, and has authentication and storage—how does it compare to Firebase? We’ll dive into that later, but here’s the quick answer: if you need SQL queries, complex data relationships, or want full control over your data, choose Supabase; if you’re building mobile apps with strong real-time sync needs, choose Firebase.


Quick Start

Creating a Supabase Project

First, head to supabase.com and sign up for an account. Once registered, click “New Project” to create a new project.

When creating a project, you’ll need to fill in a few things:

  • Project name: Choose anything, like my-first-app
  • Database password: Write this down—you’ll need it later
  • Region: Choose the closest to you. For China, Singapore or Tokyo work well

It takes about three minutes to set up. Once created, you’ll see the project dashboard with a menu on the left: Table Editor, Authentication, Storage, Edge Functions… it looks like a lot, but don’t worry—we’ll cover each one in this article.

Installation and Initialization

Now you need to install Supabase in your frontend project. First, install the dependency:

npm install @supabase/supabase-js

After installation, go to the Supabase Dashboard and find two things: Project URL and Anon Public Key. You’ll find these in Settings > API.

Then initialize the Supabase Client:

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

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

export const supabase = createClient(supabaseUrl, supabaseAnonKey);

This code is your gateway to communicating with Supabase. All subsequent operations—database queries, user login, file uploads—go through this supabase object.

Connection Testing

After initialization, let’s test if the connection works. The simplest way is to query the database for data:

const { data, error } = await supabase.from('users').select('*');

if (error) {
  console.error('Connection failed:', error.message);
} else {
  console.log('Connection successful!', data);
}

If you get an error saying the users table doesn’t exist, that’s normal—you haven’t created the table yet. The next section will show you how.

Common errors include:

  • URL or Key typo: Check if you copied the complete string
  • CORS error: Frontend projects might need CORS configuration, though Supabase allows this by default
  • Network issues: These happen occasionally—wait a few minutes and try again

If everything works, you’ll see the returned data—possibly an empty array [], meaning the table has no data yet.


Database: Database Operations

This is the core of Supabase. If you’ve used PostgreSQL before, it’ll feel familiar. If you’re a frontend developer, you might not be used to SQL, but Supabase provides a visual Table Editor, so you can operate the database without writing SQL.

Creating Tables

There are two ways to create tables: using the Table Editor (visual), or writing SQL.

Let’s start with the Table Editor. Click “Table Editor” in the left Dashboard, then click “Create a new table.” You need to fill in the table name, field names, and field types.

For example, create a users table:

Field NameTypeConstraints
idint8Primary Key, Auto Increment
emailtextUnique, Not Null
nametext-
created_attimestamptzDefault: now()

After filling in, click “Save” and the table is created.

However, I find writing SQL faster, especially for complex operations later. Supabase provides a SQL Editor, which you can find in the left Dashboard.

SQL to create users and projects tables:

-- Users table
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  email TEXT UNIQUE NOT NULL,
  name TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Projects table
CREATE TABLE projects (
  id SERIAL PRIMARY KEY,
  user_id INTEGER REFERENCES users(id),
  title TEXT NOT NULL,
  description TEXT,
  status TEXT DEFAULT 'active',
  created_at TIMESTAMPTZ DEFAULT NOW()
);

Here’s a detail: user_id in the projects table is a foreign key referencing the id in the users table. This way, each project belongs to a specific user—this is the advantage of relational databases; data relationships are clear.

CRUD Operations

Now that the tables are created, let’s try create, read, update, and delete operations.

Insert Data:

// Insert a user
const { data, error } = await supabase
  .from('users')
  .insert([
    { email: 'user@example.com', name: 'John Doe' }
  ]);

if (error) {
  console.error('Insert failed:', error.message);
} else {
  console.log('Insert successful:', data);
}

Query Data:

// Query all projects, and include user information
const { data, error } = await supabase
  .from('projects')
  .select(`
    *,
    users (
      name,
      email
    )
  `)
  .eq('status', 'active')
  .order('created_at', { ascending: false });

console.log(data);

This query is interesting: it not only queries the projects table but also pulls in related user information through the foreign key—one query gets data from both tables. In Firestore, this would require multiple queries and manual data merging.

Update Data:

const { data, error } = await supabase
  .from('projects')
  .update({ status: 'completed' })
  .eq('id', 1);

Delete Data:

const { data, error } = await supabase
  .from('projects')
  .delete()
  .eq('id', 1);

These operations are all quite intuitive. One detail: .eq() is a filter condition meaning “equals.” Supabase provides many other filter methods like .gt() (greater than), .lt() (less than), .like() (fuzzy match)—basically supporting all common SQL filters.

Row Level Security (RLS)

This part is important, especially when building user-related features.

RLS stands for Row Level Security. What does that mean? You can control users to only see their own data, not others’.

For example: the projects table has many projects, but you want users to only see their own projects after logging in, not others’.

First, enable RLS:

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

Then create a Policy:

-- Users can only view their own projects
CREATE POLICY "Users can view their own projects"
ON projects FOR SELECT
USING (user_id = auth.uid());

Here auth.uid() is a function provided by Supabase that returns the current logged-in user’s ID. This Policy means: when querying the projects table, only return records where user_id equals the current user’s ID.

Similarly, you can create insert, update, and delete policies:

-- Users can only insert their own projects
CREATE POLICY "Users can insert their own projects"
ON projects FOR INSERT
WITH CHECK (user_id = auth.uid());

-- Users can only update their own projects
CREATE POLICY "Users can update their own projects"
ON projects FOR UPDATE
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());

-- Users can only delete their own projects
CREATE POLICY "Users can delete their own projects"
ON projects FOR DELETE
USING (user_id = auth.uid());

This way, even if someone tries to maliciously query others’ data, they can’t—the database blocks it at the source. Much more secure than checking in frontend code.


Auth: User Authentication System

For authentication, Supabase has it well covered. It supports many login methods: email/password login, passwordless login (Magic Link), one-time password (OTP), social login (Google, GitHub, Apple, and 20+ platforms), phone login, and enterprise SSO.

Authentication Features Overview

Let’s first discuss the core mechanism of Supabase Auth.

It uses JWT Token—JSON Web Token. After a user successfully logs in, Supabase generates a Token, and the frontend uses this Token to request backend APIs. The backend validates the Token to confirm user identity.

Also, Supabase Auth automatically stores user information in PostgreSQL’s auth.users table—this table is created automatically, you don’t need to manage it. User ID, email, creation time, last login time… all this information is in there.

There’s also Session Persistence. What does that mean? After a user logs in, the browser remembers the login state, so next time they open the page, they don’t need to log in again. Supabase’s frontend SDK handles this automatically—you don’t need to write extra code.

Email/Password Authentication

This is the most common login method. First, registration:

// User registration
const { data, error } = await supabase.auth.signUp({
  email: 'user@example.com',
  password: 'securepassword123',
});

if (error) {
  console.error('Registration failed:', error.message);
} else {
  console.log('Registration successful:', data);
}

After successful registration, Supabase sends a confirmation email to the user’s inbox. The user clicks the link in the email, and the account is activated. This is the default behavior—you can turn it off in the Dashboard, but I don’t recommend it—email confirmation prevents malicious registrations.

Then login:

// User login
const { data, error } = await supabase.auth.signInWithPassword({
  email: 'user@example.com',
  password: 'securepassword123',
});

if (error) {
  console.error('Login failed:', error.message);
} else {
  console.log('Login successful:', data);
}

After successful login, you can retrieve user information:

// Get current logged-in user
const { data: { user } } = await supabase.auth.getUser();

console.log('Current user:', user);
// Output: { id: 'abc123', email: 'user@example.com', ... }

Finally, logout:

await supabase.auth.signOut();

After logout, the Session is cleared, and the user must log in again.

Another common requirement: password reset. Supabase provides a ready-made feature:

// Send password reset email
const { data, error } = await supabase.auth.resetPasswordForEmail(
  'user@example.com'
);

The user receives an email, clicks the link, and can reset their password. You don’t need to write additional logic for the entire process.

Social Auth

This feature is quite popular—users don’t need to fill out forms; they can log in directly with their Google or GitHub account, making for a much better experience.

First, configure social login. In the Dashboard, click Authentication > Providers, then enable the platforms you want, like Google or GitHub.

Configuration varies by platform, but the general steps are similar:

  1. Create an OAuth App in Google/GitHub
  2. Copy Client ID and Client Secret to Supabase
  3. Configure callback URL (Redirect URL)

After configuration, the frontend code is simple:

// Google login
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
});

// GitHub login
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'github',
});

After calling, the page redirects to Google/GitHub’s authorization page. The user clicks “Agree,” then redirects back to your application. At this point, the user is logged in.

You can also customize the callback URL and additional parameters:

const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://your-app.com/dashboard',
    queryParams: {
      access_type: 'offline',
      prompt: 'consent',
    }
  }
});

redirectTo is the page to redirect to after successful login; queryParams are additional OAuth parameters, like requesting offline access or asking for authorization every time.

RLS Combined with Auth

Earlier when discussing RLS, I mentioned auth.uid(). Now we can connect the dots.

After a user logs in, auth.uid() returns the user’s ID. You can use this ID in database Policies to implement “users can only access their own data.”

For example: after a user logs in and creates a project, you want the project to automatically link to the current user.

// Create project, automatically link to current user
const { data: { user } } = await supabase.auth.getUser();

const { data, error } = await supabase
  .from('projects')
  .insert([
    {
      title: 'New Project',
      user_id: user.id  // Get user ID from Auth
    }
  ]);

Then RLS Policy ensures:

  • When querying, only see projects where user_id = auth.uid()
  • When inserting, user_id must equal auth.uid()
  • Same for update and delete

This way, authentication and permission control are complete—user login, data association, permission restrictions, all connected.


Storage: File Storage

Supabase Storage is an object storage service, similar to AWS S3 but much simpler to use. Perfect for storing user avatars, images, documents, and similar files.

Storage Features Overview

There are two core concepts: Bucket and Object.

  • Bucket: Storage container, similar to a folder concept. You can create multiple Buckets, like avatars for avatars, documents for documents
  • Object: Specific files, like avatar.jpg, report.pdf

Buckets have two permission modes:

  • Public: Public bucket, anyone can access files (suitable for public images)
  • Private: Private bucket, only authorized users can access (suitable for private documents)

Creating a Bucket

Click Storage in the Dashboard, then click “New Bucket” to create a bucket.

For example, create an avatars bucket for user avatars:

  • Name: avatars
  • Public bucket: Check (avatars are usually public)
  • File size limit: Set maximum file size, like 2MB

After creation, you can upload files to this bucket.

File Upload and Download

Upload files:

// Upload user avatar
const file = document.getElementById('avatar-input').files[0];

const { data, error } = await supabase.storage
  .from('avatars')
  .upload('user-id/avatar.jpg', file, {
    cacheControl: '3600',
    upsert: false
  });

if (error) {
  console.error('Upload failed:', error.message);
} else {
  console.log('Upload successful:', data);
}

Here are a few details:

  • upload() first parameter is the file path: 'user-id/avatar.jpg'
  • cacheControl: Cache duration, 3600 seconds
  • upsert: Whether to overwrite if file exists. false means don’t overwrite, will error; true means overwrite

Download files:

// Download file
const { data, error } = await supabase.storage
  .from('avatars')
  .download('user-id/avatar.jpg');

if (error) {
  console.error('Download failed:', error.message);
} else {
  // data is a Blob object, can convert to URL for display
  const url = URL.createObjectURL(data);
  document.getElementById('avatar-img').src = url;
}

If the bucket is public, you can also get a public URL directly:

// Get public URL
const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl('user-id/avatar.jpg');

console.log('Public URL:', data.publicUrl);
// Output: https://your-project.supabase.co/storage/v1/object/public/avatars/user-id/avatar.jpg

Private bucket files need to generate temporary access URLs:

// Generate temporary access URL (valid for 1 hour)
const { data, error } = await supabase.storage
  .from('documents')
  .createSignedUrl('private-file.pdf', 3600);

console.log('Temporary URL:', data.signedUrl);

Delete files:

const { data, error } = await supabase.storage
  .from('avatars')
  .remove(['user-id/avatar.jpg']);

Access Control

Storage also supports RLS Policy to control which files users can access.

For example: users can only upload to and access files in their own folder.

-- Users can only upload to their own folder
CREATE POLICY 'Users can upload to their own folder'
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'avatars' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

-- Users can only access files in their own folder
CREATE POLICY 'Users can access their own files'
ON storage.objects FOR SELECT
USING (
  bucket_id = 'avatars' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

Here storage.foldername(name) extracts the folder part of the file path. [1] takes the first folder name, which is the user ID.

This way, when a user uploads a file, the path must be user-id/filename—RLS verifies if user-id equals the current user ID. If not, the upload is rejected.


Practical Example: Task Management Application

Having covered so much, let’s put it together with a complete example.

Suppose we want to build a simple task management application with features including:

  • User registration and login
  • Create tasks, view task list
  • Upload task attachments

Database Design

First, design the table structure:

-- Users table (Auth creates automatically, don't need to create manually)
-- Tasks table
CREATE TABLE tasks (
  id SERIAL PRIMARY KEY,
  user_id UUID REFERENCES auth.users(id) NOT NULL,
  title TEXT NOT NULL,
  description TEXT,
  status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'completed')),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

-- Policy: Users can only access their own tasks
CREATE POLICY 'Users can manage their own tasks'
ON tasks FOR ALL
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());

-- Attachments table (linked to tasks)
CREATE TABLE task_attachments (
  id SERIAL PRIMARY KEY,
  task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
  file_name TEXT NOT NULL,
  file_path TEXT NOT NULL,
  file_size INTEGER,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Enable RLS
ALTER TABLE task_attachments ENABLE ROW LEVEL SECURITY;

-- Policy: Permission through task association
CREATE POLICY 'Users can manage attachments of their tasks'
ON task_attachments FOR ALL
USING (
  EXISTS (
    SELECT 1 FROM tasks
    WHERE tasks.id = task_attachments.task_id
    AND tasks.user_id = auth.uid()
  )
);

Here’s a detail: the Policy for task_attachments doesn’t directly check user_id, but links to the tasks table through task_id, then checks tasks.user_id. This way, only the task owner can manage attachments.

Creating Storage Bucket

Create a task-files bucket and set it to private:

-- Create in Dashboard, or use SQL
INSERT INTO storage.buckets (name, public)
VALUES ('task-files', false);

-- Policy: Users can only upload to their own folder
CREATE POLICY 'Users can upload task files'
ON storage.objects FOR INSERT
WITH CHECK (
  bucket_id = 'task-files' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

CREATE POLICY 'Users can access task files'
ON storage.objects FOR SELECT
USING (
  bucket_id = 'task-files' AND
  (storage.foldername(name))[1] = auth.uid()::text
);

Core Code Implementation

Code that ties all features together:

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

const supabase = createClient(
  'https://your-project.supabase.co',
  'your-anon-key'
);

// 1. User registration
async function register(email: string, password: string) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  });

  if (error) throw error;
  return data;
}

// 2. User login
async function login(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) throw error;
  return data;
}

// 3. Create task
async function createTask(title: string, description?: string) {
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error('Not logged in');

  const { data, error } = await supabase
    .from('tasks')
    .insert([
      {
        title,
        description,
        user_id: user.id,
      }
    ])
    .select();

  if (error) throw error;
  return data[0];
}

// 4. Get task list
async function getTasks() {
  const { data, error } = await supabase
    .from('tasks')
    .select('*')
    .order('created_at', { ascending: false });

  if (error) throw error;
  return data;
}

// 5. Upload attachment
async function uploadAttachment(taskId: number, file: File) {
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error('Not logged in');

  const filePath = user.id + '/' + taskId + '/' + file.name;

  const { data, error } = await supabase.storage
    .from('task-files')
    .upload(filePath, file);

  if (error) throw error;

  // Record in task_attachments table
  const { data: attachment, error: dbError } = await supabase
    .from('task_attachments')
    .insert([
      {
        task_id: taskId,
        file_name: file.name,
        file_path: filePath,
        file_size: file.size,
      }
    ])
    .select();

  if (dbError) throw dbError;

  return attachment[0];
}

// 6. Get task attachments
async function getTaskAttachments(taskId: number) {
  const { data, error } = await supabase
    .from('task_attachments')
    .select('*')
    .eq('task_id', taskId);

  if (error) throw error;

  // Generate temporary access URLs
  const attachmentsWithURLs = data.map(async (attachment) => {
    const { data: urlData } = await supabase.storage
      .from('task-files')
      .createSignedUrl(attachment.file_path, 3600);

    return {
      ...attachment,
      url: urlData.signedUrl,
    };
  });

  return Promise.all(attachmentsWithURLs);
}

// 7. Complete task
async function completeTask(taskId: number) {
  const { data, error } = await supabase
    .from('tasks')
    .update({ status: 'completed', updated_at: new Date() })
    .eq('id', taskId)
    .select();

  if (error) throw error;
  return data[0];
}

// 8. Logout
async function logout() {
  await supabase.auth.signOut();
}

This code covers user registration/login, task CRUD, file uploads—basically the skeleton of a complete application. You can add more features on this foundation, like task categories, tags, comments, notifications…


Supabase vs Firebase Comparison

After all this, you might wonder: which should I choose, Supabase or Firebase? Let’s compare them in detail.

Core Differences

Comparison AspectSupabaseFirebase
Database TypePostgreSQL (relational)Firestore (document-based NoSQL)
Open SourceCompletely open sourceGoogle’s closed-source product
Pricing ModelFixed $25/month (Pro Plan)Usage-based billing (read/write/storage separate)
Data Ownership100% ownership, export anytimeData on Google, export troublesome
Query CapabilitySQL powerful queries, complex relationshipsLimited queries, complex relationships need multiple queries
Real-time FeaturesWebSocket, manual subscription neededFirestore native real-time, auto sync
Offline SupportNeed to implement caching yourselfNative offline support, auto sync
Migration DifficultyStandard PostgreSQL, easy migrationProprietary format, high migration cost

Choose Supabase when:

  • Complex data relationships, need SQL queries and associations
  • Long-term projects, want full data control
  • Team familiar with SQL, want to use PostgreSQL
  • Cost-sensitive, limited budget (fixed costs more stable than usage-based)
  • Open-source priority, want controllable tech stack

Choose Firebase when:

  • Rapid prototyping, tight timeline
  • Mobile-first applications, offline features important
  • Real-time sync is core functionality (like chat apps)
  • Already using Google Cloud ecosystem
  • Simple data structure, queries not complex

Honestly, I used Firebase for several projects before. Later, the bill surprised me—over $800 a month. After migrating to Supabase for the same functionality, costs stabilized around $25. That’s a significant difference.

Also, data export from Firebase is troublesome—Firestore’s data format is proprietary, requiring conversion after export to use. Supabase is simple—export SQL files directly, or use PostgreSQL tools to export CSV, can migrate to other databases anytime.

However, Firebase is indeed stronger in real-time features and mobile applications—Firestore’s real-time sync and offline support are well done. If you’re building chat apps, collaborative documents with strong real-time needs, Firebase might be more convenient.


Summary and Recommendations

Let’s wrap this up.

Supabase’s core values are:

  • PostgreSQL database—powerful queries, clear relationships
  • Enterprise-grade authentication—multiple login methods, JWT Token, RLS permissions
  • Object storage—simple to use, supports access control
  • Real-time sync, edge computing—can explore later

And it’s open source, with fully controllable data and stable costs—these matter for long-term projects.

If you’re a frontend developer wanting to quickly build full-stack applications without wrestling with backend configuration, Supabase is a solid choice. You can essentially get database, authentication, and storage working in a few hours—much faster than traditional backend development.

Learning recommendations:

  • First, get clear on Database, Auth, Storage—these are the core
  • Then try RLS Policy—this helps you understand how authentication and permission control connect
  • Practical projects are the best way to learn—find a need and build an application from scratch
  • Later explore advanced features like Realtime, Edge Functions, Vector Database

That about covers it. Supabase’s official documentation is well written—if you run into something you don’t understand, check it out: supabase.com/docs. There are also many tutorials and examples in the community, like the awesome-supabase list on GitHub with curated resources.

Bottom line: if you want to use PostgreSQL for a backend without wrestling with configuration, Supabase has paved the way—just walk it.


Getting Started with Supabase Three Core Features

Build a Supabase project from scratch and master three core features: Database, Auth, and Storage

⏱️ Estimated time: 2 hr

  1. 1

    Step1: Create Supabase Project

    Go to supabase.com to register an account and create a new project:

    • Project name: my-first-app
    • Database password: Must remember this
    • Region: Choose closest to you (Singapore or Tokyo for China)

    After creation, get Project URL and Anon Public Key in Settings > API
  2. 2

    Step2: Install and Initialize Client

    Install dependency in frontend project:

    npm install @supabase/supabase-js

    Initialize Supabase Client:

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

    const supabase = createClient(
    'https://your-project.supabase.co',
    'your-anon-key'
    );
  3. 3

    Step3: Create Tables and Configure RLS

    Use SQL Editor to create tables:

    CREATE TABLE tasks (
    id SERIAL PRIMARY KEY,
    user_id UUID REFERENCES auth.users(id),
    title TEXT NOT NULL,
    status TEXT DEFAULT 'pending'
    );

    ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;

    CREATE POLICY 'Users can manage their own tasks'
    ON tasks FOR ALL
    USING (user_id = auth.uid());

    RLS Policy ensures users can only access their own data
  4. 4

    Step4: Implement User Authentication

    Email/Password authentication code examples:

    // Registration
    await supabase.auth.signUp({
    email: 'user@example.com',
    password: 'password123'
    });

    // Login
    await supabase.auth.signInWithPassword({
    email: 'user@example.com',
    password: 'password123'
    });

    // Get current user
    const { data: { user } } = await supabase.auth.getUser();

    Supports Google, GitHub, and 20+ platform social logins
  5. 5

    Step5: Configure File Storage

    Create Storage Bucket (public or private):

    // Upload file
    await supabase.storage
    .from('avatars')
    .upload('user-id/avatar.jpg', file);

    // Get public URL
    const { data } = supabase.storage
    .from('avatars')
    .getPublicUrl('user-id/avatar.jpg');

    Private files use createSignedUrl to generate temporary access links

FAQ

What are the core differences between Supabase and Firebase?
Supabase uses PostgreSQL relational database supporting complex SQL queries and data relationships; Firebase uses Firestore document-based NoSQL with limited query capabilities. Supabase is completely open source with fixed pricing at $25/month and 100% data control; Firebase is closed-source with usage-based billing (potentially $800+/month) and troublesome data export.
What kind of projects is Supabase suitable for?
Suitable for these scenarios:

• Complex data relationships requiring SQL queries and associations
• Long-term projects wanting full data control
• Cost-sensitive with limited budget (fixed costs more stable)
• Frontend developers quickly building full-stack applications
• Open-source priority wanting controllable tech stack
What is Row Level Security (RLS) and what is it for?
RLS is PostgreSQL's row-level security feature that controls at the database level which data users can access. Combined with Supabase Auth's auth.uid() function, it implements that logged-in users can only see/modify records they created—more secure than frontend code checks.
What login methods does Supabase Auth support?
Supports multiple login methods:

• Email/Password
• Magic Link (passwordless login)
• OTP (one-time password)
• Social Auth (Google, GitHub, Apple, and 20+ platforms)
• Phone Auth (Twilio, MessageBird)
• SSO (enterprise single sign-on)
What's the difference between public and private buckets in Supabase Storage?
Public Bucket files can be accessed by anyone, suitable for public images, avatars, etc.; Private Bucket files require authorization to access, using createSignedUrl to generate temporary access links (with settable expiration), suitable for private documents, user-uploaded files, etc.
How long does it take to migrate from Firebase to Supabase?
Simple applications can migrate in 2-3 days: convert Firestore data to PostgreSQL table structure, export Firebase Auth users to Supabase Auth, migrate Cloud Storage files to Supabase Storage. Complex applications may need 1-2 weeks, mainly for data model conversion and query logic rewriting.
What does Supabase's free plan include?
The free plan includes: unlimited API requests, 50,000 monthly active users, 500MB database storage, 1GB file storage, 5GB bandwidth. Suitable for personal MVP development; exceeding limits requires upgrade to Pro Plan ($25/month).

15 min read · Published on: Apr 3, 2026 · Modified on: Apr 5, 2026

Comments

Sign in with GitHub to leave a comment

Related Posts