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.
In this guide, we’ll explore its core features and how they fit into a Next.js application.
Next.js│▼Supabase Client│┌──────────────┼──────────────┐▼ ▼ ▼PostgreSQL Storage Auth│├── Tables and Relations├── Row Level Security (RLS)├── Triggers└── pgvector│├──────────→ Realtime│└──────── ──→ Database Webhooks│▼Edge Functions│▼External APIs
PostgreSQL is the main database for storing structured application data, such as posts, users, products, orders, and categories.
Create a posts table:
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 stores files such as images, videos, PDFs, and documents. PostgreSQL typically stores the file path or metadata, while Storage holds the actual file.
Create a bucket named blog-media, then upload a file:
const { data, error } = await supabase.storage.from("blog-media").upload("posts/monstera.webp", file);
Generate a public URL:
const { data } = await supabase.storage.from("blog-media").getPublicUrl("posts/monstera.webp");
A typical structure looks like this:
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 manages user authentication and supports email/password, magic links, one-time passwords (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, error } = await supabase.auth.getUser();const user = data.user;
The basic flow is:
User → Supabase Auth → Authenticated User → Protected Data
In Next.js, authentication should be integrated with an appropriate server-side session strategy when protecting server-rendered pages or backend operations.
Row Level Security controls which users can read, insert, update, or delete specific database rows.
Enable RLS on the posts table:
alter table posts enable row level security;
For example, users can be allowed to update only their own posts:
create policy "Users can update own posts"on postsfor updateusing (auth.uid() = user_id)with check (auth.uid() = user_id);
This example assumes the table has a user_id column identifying each post’s owner.
The flow is:
User → Supabase → RLS Check → Database├── Allowed└── Rejected
RLS is particularly important when the frontend accesses Supabase directly. Policies should be designed for every operation the application permits, rather than relying only on frontend checks.
Supabase automatically provides a data API for PostgreSQL tables, allowing you to perform common CRUD operations without manually creating an endpoint for each table.
Read:
const { data, error } = await supabase.from("posts").select("*");
Insert:
const { data, error } = await supabase.from("posts").insert({title: "My Post",slug: "my-post",});
Update:
const { data, error } = 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 Route Handlers, Server Actions, or Supabase Edge Functions.
Supabase Realtime allows the frontend to receive database changes 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:
The basic flow is:
PostgreSQL → Realtime → Frontend
Make sure the relevant tables and permissions are configured for the changes your application needs to receive.
Supabase Edge Functions are server-side functions useful for logic that should not run in the browser.
Common use cases include:
Create a function:
supabase functions new hello
Example implementation:
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",},});
The flow is:
Next.js → Edge Function → External API → Response
Keep secret keys on the server side and apply appropriate authentication and authorization to functions that perform sensitive operations.
Database Webhooks send HTTP requests when configured database events occur.
For example, when an order is created:
orders INSERT↓Database Webhook↓Edge Function↓Send Notification
You can configure a webhook through the Supabase Dashboard:
Supabase Dashboard→ Database→ Webhooks→ Create Webhook
Select the table and event, such as:
Table: ordersEvent: INSERT
Then specify the target endpoint, for example:
https://project.supabase.co/functions/v1/order-created
The key difference is:
Realtime → Database → FrontendWebhook → Database → HTTP Endpoint
Realtime is useful for delivering updates to connected clients. Webhooks are useful for triggering backend workflows in response to database events.
A PostgreSQL trigger automatically executes database logic when a configured event occurs.
For example, you can create a profile record whenever a user signs up. Supabase Auth stores users in auth.users, while application profile data can live in public.profiles.
First, define the function:
create function public.create_profile()returns triggerlanguage plpgsqlsecurity definerset search_path = ''as $$begininsert into public.profiles (id)values (new.id);return new;end;$$;
Then 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 maintaining timestamps, creating related records, or recording audit information.
Because this function uses security definer, its permissions and execution context should be reviewed carefully. Keep its privileges minimal and ensure the target table has appropriate access controls.
The pgvector extension allows PostgreSQL to store and search vector embeddings. This makes Supabase useful for AI applications that require semantic search or Retrieval-Augmented Generation (RAG).
Enable the extension:
create extension if not exists vector;
Create a table:
create table documents (id uuid primary key,content text,embedding vector(1536));
The dimension 1536 must match the embedding model you use.
A typical RAG pipeline looks like this:
Document↓Embedding Model↓Vector↓PostgreSQL + pgvector↓Similarity Search↓Relevant Documents↓LLM↓Answer
A similarity search can look like this:
selectid,content,1 - (embedding <=> query_embedding) as similarityfrom documentsorder by embedding <=> query_embeddinglimit 5;
Here, query_embedding represents a vector supplied by the application. In a real query, it must be passed or defined using a compatible vector type.
This approach is useful for RAG applications, AI chatbots, semantic search, and knowledge bases.
Database migrations store schema changes as version-controlled files. This makes changes reproducible across development, staging, and production environments.
Create a migration:
supabase migration new add_slug
Add the SQL:
alter table postsadd column slug text;
Apply local migrations to the remote database:
supabase db push
The typical workflow is:
Migration → Git → Deployment → Production Database
Migrations are especially useful when working in teams because database changes can be reviewed, tracked, and applied consistently.
Supabase can generate TypeScript types from your PostgreSQL schema, helping keep database queries aligned with your application code.
Generate types:
supabase gen types typescript \--project-id YOUR_PROJECT_ID \> database.types.ts
A database table might look like this:
posts├── id├── title└── status
The generated types describe its columns and their corresponding database types. For example, a simplified representation might be:
type Post = {id: string;title: string;status: string;};
Use the generated Database type with the Supabase client:
import { createClient } from "@supabase/supabase-js";import type { Database } from "./database.types";const supabase = createClient<Database>(SUPABASE_URL,SUPABASE_KEY);
The flow is:
PostgreSQL Schema↓Supabase CLI↓database.types.ts↓Next.js + TypeScript
The generated types provide better autocomplete and compile-time checks. They do not replace runtime validation of untrusted input.
The Supabase CLI is a 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 typescriptsupabase functions deploy hello
The CLI is especially useful when database schemas, Edge Functions, and other backend changes need to be managed through Git.
Supabase Logs help you investigate errors and understand what is happening inside your project.
Logs can help troubleshoot:
You can access them through:
Supabase Dashboard→ Logs
Monitoring focuses on the overall health and usage of the system, including:
Database UsageAPI RequestsError RatePerformanceStorage Usage
A simple debugging workflow is:
Problem↓Supabase Logs↓Find Request or Error↓Identify Cause↓Fix
Combining logs with application-level monitoring makes it easier to identify and investigate production issues.
Supabase provides a broad set of backend capabilities around PostgreSQL, from database operations and authentication to file storage, realtime updates, serverless functions, and vector search.
For a Next.js application, these features can reduce the amount of backend infrastructure you need to build and maintain yourself.
Start with the database, authentication, and RLS. Then introduce Storage, Realtime, Edge Functions, or pgvector as your application’s requirements grow.
The key is to choose the features that solve your current problems while maintaining clear boundaries, secure access controls, and a reproducible development workflow.