Supabase is a backend platform built around PostgreSQL. It provides a database, file storage, authentication, APIs, realtime capabilities, serverless functions, and developer tools in one platform. This guide gives a practical overview of the main features and how to implement them in a Next.js application.
FlowNEXT.JS│▼SUPABASE CLIENT│┌─────────────────┼─────────────────┐│ │ │▼ ▼ ▼PostgreSQL Storage Auth│├── Tables├── Relations├── RLS├── Triggers└── pgvector│├──────────────→ Realtime│└──────────────→ Webhooks│▼Edge Functions│▼External APIs
PostgreSQL is the main database used to store structured application data such as posts, users, products, orders, and categories.
create table posts (id uuid primary key default gen_random_uuid(),slug text unique not null,title text not null,content text,created_at timestamptz default now());
Read data from Next.js:
const { data, error } = await supabase.from("posts").select("*");
The basic flow is:
Next.js → Supabase Client → PostgreSQL → posts
Supabase Storage is used to store files such as images, videos, PDFs, and documents. PostgreSQL normally stores the file path or metadata, while Storage stores the actual file.
Create a bucket such as blog-media, then upload a file:
const { data, error } = await supabase.storage.from("blog-media").upload("posts/monstera.webp", file);
Get a public URL:
const { data } = supabase.storage.from("blog-media").getPublicUrl("posts/monstera.webp");
Typical structure:
PostgreSQL└── cover_image = "posts/monstera.webp"Storage└── blog-media/posts/monstera.webp
Use private buckets and signed URLs when files should not be publicly accessible.
Supabase Auth handles user authentication and supports email/password, magic links, OTP, and OAuth providers.
Sign up:
const { data, error } = await supabase.auth.signUp({email,password,});
Sign in:
const { data, error } =await supabase.auth.signInWithPassword({email,password,});
Get the current user:
const { data } = await supabase.auth.getUser();const user = data.user;
The basic flow is:
User → Supabase Auth → Authenticated User → Protected Data
RLS controls which users can read, insert, update, or delete specific database rows.
Enable RLS:
alter table posts enable row level security;
For example, users can only update their own posts:
create policy "Users can update own posts"on postsfor updateusing (auth.uid() = user_id);
The flow is:
User → Supabase → RLS Check → Database│├── Allowed└── Rejected
RLS is especially important when the client accesses Supabase directly.
Supabase automatically provides an API for PostgreSQL tables, so you do not need to manually create basic CRUD endpoints.
Read:
const { data } = await supabase.from("posts").select("*");
Insert:
await supabase.from("posts").insert({title: "My Post",});
Update:
await supabase.from("posts").update({title: "Updated Post",}).eq("id", postId);
The basic flow is:
Next.js → Supabase API → PostgreSQL
For complex business logic, you may still need Next.js API routes, Server Actions, or Edge Functions.
Supabase Realtime allows the frontend to receive database changes immediately without repeatedly polling the database.
For example, an order status can update automatically when the database changes.
supabase.channel("orders").on("postgres_changes",{event: "*",schema: "public",table: "orders",},(payload) => {console.log(payload);}).subscribe();
Typical use cases include chat, notifications, live dashboards, and order status updates.
PostgreSQL → Realtime → Frontend
Edge Functions are server-side functions that run on Supabase. They are useful for backend logic that should not run in the browser.
Common use cases include:
Create a function:
supabase functions new hello
Example:
Deno.serve(async (req) => {return new Response(JSON.stringify({message: "Hello!",}),{headers: {"Content-Type": "application/json",},});});
Deploy it:
supabase functions deploy hello
Call it from Next.js:
const { data, error } =await supabase.functions.invoke("hello", {body: {name: "Viet",},});
Typical flow:
Next.js → Edge Function → External API → Response
Database Webhooks automatically send an HTTP request when a database event occurs.
For example:
orders INSERT↓Database Webhook↓Edge Function↓Send notification
Create one from:
Supabase Dashboard→ Database→ Webhooks→ Create Webhook
Select the table and event, such as:
Table: ordersEvent: INSERT
Then specify the target URL:
https://project.supabase.co/functions/v1/order-created
The key difference is:
Realtime → Database → FrontendWebhook → Database → HTTP endpoint
A database trigger automatically executes PostgreSQL logic when a database event occurs.
For example, automatically create a profile when a user signs up. Supabase stores users in auth.users, so the function runs as security definer to be allowed to write into public.profiles:
create function public.create_profile()returns triggerlanguage plpgsqlsecurity definer set search_path = ''as $$begininsert into public.profiles (id)values (new.id);return new;end;$$;
Create the trigger:
create trigger on_auth_user_createdafter insert on auth.usersfor each rowexecute function public.create_profile();
The flow is:
INSERT auth.users → Trigger → PostgreSQL Function → INSERT profiles
Use triggers for database-level automation such as timestamps, audit records, or creating related records.
pgvector is a PostgreSQL extension that allows you to store and search embeddings. It can turn PostgreSQL into a vector database for AI applications.
Enable it:
create extension if not exists vector;
Create a table:
create table documents (id uuid primary key,content text,embedding vector(1536));
The RAG flow is:
Document↓Embedding Model↓Vector↓PostgreSQL + pgvector↓Similarity Search↓Relevant Documents↓LLM↓Answer
A similarity search can look like:
selectid,content,1 - (embedding <=> query_embedding) as similarityfrom documentsorder by embedding <=> query_embeddinglimit 5;
This is useful for RAG, AI chatbots, semantic search, and knowledge bases.
Migrations store database schema changes as code. This makes database changes version-controlled and reproducible.
Create a migration:
supabase migration new add_slug
Add the SQL:
alter table postsadd column slug text;
Apply migrations:
supabase db push
Typical workflow:
Migration → Git → Deployment → Production Database
Migrations are especially useful when working with teams or multiple environments.
Supabase can generate TypeScript types directly from your PostgreSQL schema.
Generate types:
supabase gen types typescript \--project-id YOUR_PROJECT_ID \> database.types.ts
For example, a database table:
posts├── id├── title└── status
can become:
type Post = {id: string;title: string;status: string;};
Use the generated type with the Supabase client:
const supabase =createClient<Database>(SUPABASE_URL,SUPABASE_KEY);
The flow is:
PostgreSQL Schema↓Supabase CLI↓database.types.ts↓Next.js + TypeScript
This helps keep application code synchronized with the database schema.
Supabase CLI is the command-line tool for developing and managing Supabase projects.
Common tasks include:
Supabase CLI├── Local development├── Database migrations├── Generate TypeScript types├── Deploy Edge Functions└── Database management
Common commands:
supabase startsupabase migration new add_postssupabase db pushsupabase gen types typescript ...supabase functions deploy hello
The CLI is especially useful when database changes and backend code need to be managed through Git.
Supabase Logs help you understand what is happening inside your project and troubleshoot errors.
Logs can help investigate:
You can access them through:
Supabase Dashboard→ Logs
Monitoring focuses more on the overall health and usage of the system, such as:
Database usageAPI requestsError ratePerformanceStorage usage
A simple production debugging flow is:
Problem↓Supabase Logs↓Find request/error↓Identify cause↓Fix