Supabase has rapidly emerged as a powerful open-source alternative to Firebase, providing developers with a suite of tools built around a PostgreSQL database. At its core, Supabase offers an instant, real-time API layer on top of your database, significantly accelerating backend development. This guide provides a comprehensive overview of how to leverage the Supabase API, and cover everything from initial setup and basic operations to security, customization, and type safety.
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demans, and replaces Postman at a much more affordable price!

1. Introduction: What is the Supabase API?
Unlike traditional backend development where you might spend considerable time building REST or GraphQL endpoints to interact with your database, Supabase automatically generates a secure and performant API for you. When you create a table in your Supabase PostgreSQL database, Supabase uses PostgREST, an open-source tool, to introspect your database schema and provide corresponding RESTful endpoints.
Key Benefits:
- Instant Backend: Get functional API endpoints as soon as you define your database schema.
- Real-time Capabilities: Subscribe to database changes via WebSockets.
- Based on PostgreSQL: Leverage the power, flexibility, and maturity of PostgreSQL, including features like Row Level Security (RLS).
- Multiple Interaction Methods: Interact via REST, GraphQL (community supported), or Supabase's client libraries (JavaScript, Python, Dart, etc.).
- Extensibility: Create custom serverless functions (Edge Functions) for complex logic or integrations.
This guide focuses primarily on the REST API and its interaction via client libraries, as well as Supabase Edge Functions.
2. Getting Started with Supabase API
The easiest way to understand the Supabase API is to jump right in. Let's assume you have a Supabase project set up (if not, visit supabase.com and create one for free) and have created a simple table, for example, profiles
:
-- Create a table for public profiles
create table profiles (
id uuid references auth.users not null primary key,
updated_at timestamp with time zone,
username text unique,
avatar_url text,
website text,
constraint username_length check (char_length(username) >= 3)
);
-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table profiles
enable row level security;
create policy "Public profiles are viewable by everyone." on profiles
for select using (true);
create policy "Users can insert their own profile." on profiles
for insert with check (auth.uid() = id);
create policy "Users can update own profile." on profiles
for update using (auth.uid() = id);
-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth.
-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details.
create function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, username)
values (new.id, new.raw_user_meta_data->>'username');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
(Note: The example above uses profiles
, aligning with Supabase's standard examples. The concept applies equally to a todos
table or any other table you create.)
Finding Your API Credentials:
Every Supabase project comes with unique API credentials:
- Project URL: Your unique Supabase endpoint (e.g.,
https://<your-project-ref>.supabase.co
). - API Keys: Found in your Supabase Project Dashboard under
Project Settings
>API
.
anon
(public) key: This key is safe to use in client-side applications (like browsers or mobile apps). It relies on Row Level Security (RLS) to control data access.service_role
key: This is a secret key with full administrative privileges, bypassing RLS. Never expose this key in client-side code. Use it only in secure server environments (like backend servers or serverless functions).
Interacting with the API (using Supabase JS Client Library):
Supabase provides client libraries to simplify API interactions. Here's how you'd use the JavaScript library (supabase-js
):
// 1. Import and initialize the client
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = 'https://<your-project-ref>.supabase.co'
const supabaseAnonKey = '<your-anon-key>'
const supabase = createClient(supabaseUrl, supabaseAnonKey)
// 2. Fetch data (SELECT *)
async function getProfiles() {
const { data, error } = await supabase
.from('profiles')
.select('*')
if (error) console.error('Error fetching profiles:', error)
else console.log('Profiles:', data)
}
// 3. Insert data (INSERT)
async function createProfile(userId, username) {
const { data, error } = await supabase
.from('profiles')
.insert([
{ id: userId, username: username, updated_at: new Date() },
])
.select() // Return the inserted data
if (error) console.error('Error creating profile:', error)
else console.log('Created Profile:', data)
}
// 4. Update data (UPDATE)
async function updateProfileUsername(userId, newUsername) {
const { data, error } = await supabase
.from('profiles')
.update({ username: newUsername, updated_at: new Date() })
.eq('id', userId) // Only update where id matches
.select()
if (error) console.error('Error updating profile:', error)
else console.log('Updated Profile:', data)
}
// 5. Delete data (DELETE)
async function deleteProfile(userId) {
const { data, error } = await supabase
.from('profiles')
.delete()
.eq('id', userId) // Only delete where id matches
if (error) console.error('Error deleting profile:', error)
// Note: Delete often returns minimal data on success unless .select() is used *before* .delete() on some versions/setups.
else console.log('Profile deleted successfully')
}
// Example Usage (assuming you have a user ID)
// getProfiles();
// createProfile('some-uuid-v4', 'new_user');
// updateProfileUsername('some-uuid-v4', 'updated_username');
// deleteProfile('some-uuid-v4');
This quickstart demonstrates the fundamental CRUD (Create, Read, Update, Delete) operations using the client library, which internally calls the REST API.
3. The Auto-Generated Supabase REST API
While client libraries are convenient, understanding the underlying REST API generated by PostgREST is crucial.
API Endpoint Structure:
The base URL for the REST API is typically: https://<your-project-ref>.supabase.co/rest/v1/
Endpoints are automatically created for your tables:
GET /rest/v1/your_table_name
: Retrieves rows from the table.POST /rest/v1/your_table_name
: Inserts new rows into the table.PATCH /rest/v1/your_table_name
: Updates existing rows in the table.DELETE /rest/v1/your_table_name
: Deletes rows from the table.
Authentication:
API requests must include your API key in the apikey
header and usually an Authorization
header containing Bearer <your-api-key>
(often the same anon
key for client-side requests, or the service_role
key for server-side).
apikey: <your-anon-or-service-role-key>
Authorization: Bearer <your-anon-or-service-role-key>
Common Operations (using curl
examples):
Let's replicate the previous examples using curl
against the REST API directly. Replace placeholders accordingly.
Fetch Data (GET):
curl 'https://<ref>.supabase.co/rest/v1/profiles?select=*' \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>"
Insert Data (POST):
curl 'https://<ref>.supabase.co/rest/v1/profiles' \
-X POST \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \# Optional: Returns the inserted row(s) \
-d '{ "id": "some-uuid", "username": "rest_user" }'
Update Data (PATCH): (Update profile where username is 'rest_user')
curl 'https://<ref>.supabase.co/rest/v1/profiles?username=eq.rest_user' \
-X PATCH \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{ "website": "https://example.com" }'
Delete Data (DELETE): (Delete profile where username is 'rest_user')
curl 'https://<ref>.supabase.co/rest/v1/profiles?username=eq.rest_user' \
-X DELETE \
-H "apikey: <anon-key>" \
-H "Authorization: Bearer <anon-key>"
Filtering, Selecting, Ordering, Pagination:
The REST API supports powerful querying via URL parameters:
- Selecting Columns:
?select=column1,column2
- Filtering (Equality):
?column_name=eq.value
(e.g.,?id=eq.some-uuid
) - Filtering (Other Operators):
gt
(greater than),lt
(less than),gte
,lte
,neq
(not equal),like
,ilike
(case-insensitive like),in
(e.g.,?status=in.(active,pending)
) - Ordering:
?order=column_name.asc
or?order=column_name.desc
(add.nullsfirst
or.nullslast
if needed) - Pagination:
?limit=10&offset=0
(fetch first 10),?limit=10&offset=10
(fetch next 10)
Auto-Generated API Documentation:
One of Supabase's most helpful features is the auto-generated API documentation available directly within your project dashboard.
- Navigate to your Supabase project.
- Click the API Docs icon (usually looks like
<>
) in the left sidebar. - Select a table under "Tables and Views".
- You'll see detailed documentation for the REST endpoints specific to that table, including:
- Example requests (Bash/
curl
, JavaScript). - Available filters, selectors, and modifiers.
- Descriptions of columns and data types.
This interactive documentation is invaluable for understanding how to structure your API calls.
4. Generating Types for Enhanced Development Using Supabase API
For projects using TypeScript or other typed languages, Supabase provides a way to generate type definitions directly from your database schema. This brings significant benefits:
- Type Safety: Catch errors at compile time rather than runtime.
- Autocompletion: Get intelligent suggestions in your code editor for table names, column names, and function parameters.
- Improved Maintainability: Types serve as living documentation for your data structures.
Generating Types using Supabase CLI:
- Install Supabase CLI: Follow the instructions at
https://supabase.com/docs/guides/cli
. - Log in:
supabase login
- Link your project:
supabase link --project-ref <your-project-ref>
(Run this in your local project directory). You might need to provide a database password. - Generate types:
supabase gen types typescript --linked > src/database.types.ts
# Or specify project-id if not linked or in a different context
# supabase gen types typescript --project-id <your-project-ref> > src/database.types.ts
This command inspects your linked Supabase project's database schema and outputs a TypeScript file (database.types.ts
in this example) containing interfaces for your tables, views, and function arguments/return types.
Using Generated Types:
You can then import these types into your application code:
import { createClient } from '@supabase/supabase-js'
// Import the generated types
import { Database } from './database.types' // Adjust path as needed
const supabaseUrl = 'https://<your-project-ref>.supabase.co'
const supabaseAnonKey = '<your-anon-key>'
// Provide the Database type to createClient
const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey)
// Now you get type safety and autocompletion!
async function getSpecificUserProfile(username: string) {
// Autocompletes table names ('profiles')
const { data, error } = await supabase
.from('profiles')
// Autocompletes column names ('id', 'username', 'website')
.select('id, username, website')
// Type checks the value against the column type
.eq('username', username)
.single() // Expects a single result or null
if (error) {
console.error('Error fetching profile:', error)
return null;
}
// 'data' is now correctly typed based on your select query
if (data) {
console.log(`User ID: ${data.id}, Website: ${data.website}`);
// console.log(data.non_existent_column); // <-- TypeScript error!
}
return data;
}
Generating types is a highly recommended practice for robust application development with Supabase.
5. Creating Custom Supabase API Routes with Edge Functions
While the auto-generated REST API covers standard CRUD operations, you'll often need custom server-side logic for:
- Integrating with third-party services (e.g., Stripe, Twilio).
- Performing complex computations or data aggregations.
- Running logic that requires elevated privileges (
service_role
key) without exposing the key to the client. - Enforcing complex business rules.
Supabase Edge Functions provide a way to deploy Deno-based TypeScript functions globally at the edge, close to your users.
Creating an Edge Function:
- Initialize Functions (if not already done):
supabase functions new hello-world
(run in your linked project directory). This creates asupabase/functions/hello-world/index.ts
file.
Write your function code:
// supabase/functions/hello-world/index.ts
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts' // Use appropriate std version
serve(async (req) => {
// You can access request headers, method, body etc. from 'req'
console.log(`Request received for: ${req.url}`);
// Example: Accessing Supabase DB from within the function
// Note: Requires setting up the Supabase client *inside* the function
// Use environment variables for secrets!
// import { createClient } from '@supabase/supabase-js'
// const supabaseAdmin = createClient(
// Deno.env.get('SUPABASE_URL') ?? '',
// Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
// )
// const { data: users, error } = await supabaseAdmin.from('profiles').select('*').limit(10);
const data = {
message: `Hello from the Edge!`,
// users: users // Example if fetching data
}
return new Response(
JSON.stringify(data),
{ headers: { 'Content-Type': 'application/json' } },
)
})
Deploy the function:
supabase functions deploy hello-world --no-verify-jwt
# Use --no-verify-jwt for publicly accessible functions
# Omit it or set --verify-jwt=true to require a valid Supabase Auth JWT
Invoke the function:
You can call deployed functions via HTTP POST requests (or GET, depending on the function logic) to their unique endpoint:https://<your-project-ref>.supabase.co/functions/v1/hello-world
Using curl
:
curl -X POST 'https://<ref>.supabase.co/functions/v1/hello-world' \
-H "Authorization: Bearer <user-jwt-if-required>" \
-H "Content-Type: application/json" \
-d '{"name": "Functions"}' # Optional request body
Or using the Supabase JS client:
const { data, error } = await supabase.functions.invoke('hello-world', {
method: 'POST', // or 'GET', etc.
body: { name: 'Functions' }
})
Edge Functions are a powerful tool for extending your Supabase backend capabilities beyond simple database operations.
6. API Keys and Securing Your Supabase API
Understanding API keys and implementing proper security measures is non-negotiable.
API Keys Recap:
anon
(public) key: For client-side use. Relies entirely on Row Level Security (RLS) for data protection.service_role
key: For server-side use ONLY. Bypasses RLS, granting full database access. Guard this key carefully.
The Crucial Role of Row Level Security (RLS):
RLS is the cornerstone of Supabase security when using the anon
key. It allows you to define fine-grained access control policies directly within the PostgreSQL database. Policies are essentially SQL rules that determine which rows a user can view, insert, update, or delete based on their authenticated status, user ID, role, or other criteria.
Enabling RLS:
By default, RLS is disabled on new tables. You must enable it for any table you intend to access from the client-side using the anon
key.
-- Enable RLS on the 'profiles' table
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- IMPORTANT: If no policies are defined after enabling RLS,
-- access is implicitly denied for all operations (except the table owner).
Creating RLS Policies:
Policies define the USING
clause (for read access like SELECT
, UPDATE
, DELETE
) and the WITH CHECK
clause (for write access like INSERT
, UPDATE
).
Example 1: Allow logged-in users to read all profiles:
CREATE POLICY "Allow authenticated read access"
ON profiles FOR SELECT
USING ( auth.role() = 'authenticated' );
Example 2: Allow users to view only their own profile:
CREATE POLICY "Allow individual read access"
ON profiles FOR SELECT
USING ( auth.uid() = id ); -- Assumes 'id' column matches the user's UUID from Supabase Auth
Example 3: Allow users to update only their own profile:
CREATE POLICY "Allow individual update access"
ON profiles FOR UPDATE
USING ( auth.uid() = id ) -- Specifies which rows can be targeted for update
WITH CHECK ( auth.uid() = id ); -- Ensures any *new* data still matches the condition
Example 4: Allow users to insert their own profile:
CREATE POLICY "Allow individual insert access"
ON profiles FOR INSERT
WITH CHECK ( auth.uid() = id );
You can view, create, and manage RLS policies directly in the Supabase Dashboard under Authentication
> Policies
.
Key Security Principles:
- Always enable RLS on tables accessed via the
anon
key. - Define explicit policies for
SELECT
,INSERT
,UPDATE
,DELETE
as needed. Start with restrictive policies and open up access carefully. - Never expose the
service_role
key in client-side code or insecure environments. - Use Edge Functions for operations requiring elevated privileges or complex server-side logic, protecting your
service_role
key within the function's secure environment variables. - Regularly review your RLS policies to ensure they meet your application's security requirements.
7. Mapping SQL Concepts to the Supabase API (SQL to API)
If you're familiar with SQL, understanding how common SQL operations map to the Supabase API (both REST and client libraries) is helpful.
SELECT * FROM my_table;
- REST:
GET /rest/v1/my_table?select=*
- JS:
supabase.from('my_table').select('*')
SELECT column1, column2 FROM my_table WHERE id = 1;
- REST:
GET /rest/v1/my_table?select=column1,column2&id=eq.1
- JS:
supabase.from('my_table').select('column1, column2').eq('id', 1)
INSERT INTO my_table (column1, column2) VALUES ('value1', 'value2');
- REST:
POST /rest/v1/my_table
with JSON body{"column1": "value1", "column2": "value2"}
- JS:
supabase.from('my_table').insert({ column1: 'value1', column2: 'value2' })
UPDATE my_table SET column1 = 'new_value' WHERE id = 1;
- REST:
PATCH /rest/v1/my_table?id=eq.1
with JSON body{"column1": "new_value"}
- JS:
supabase.from('my_table').update({ column1: 'new_value' }).eq('id', 1)
DELETE FROM my_table WHERE id = 1;
- REST:
DELETE /rest/v1/my_table?id=eq.1
- JS:
supabase.from('my_table').delete().eq('id', 1)
Joins: While direct SQL JOIN
syntax isn't used in the basic REST calls, you can fetch related data using:
- Foreign Key Relationships:
?select=*,related_table(*)
fetches data from related tables defined by foreign keys. - JS:
supabase.from('my_table').select('*, related_table(*)')
- RPC (Remote Procedure Calls): For complex joins or logic, create a PostgreSQL function and call it via the API.
-- Example SQL function
CREATE FUNCTION get_user_posts(user_id uuid)
RETURNS TABLE (post_id int, post_content text) AS $$
SELECT posts.id, posts.content
FROM posts
WHERE posts.author_id = user_id;
$$ LANGUAGE sql;
- REST:
POST /rest/v1/rpc/get_user_posts
with JSON body{"user_id": "some-uuid"}
- JS:
supabase.rpc('get_user_posts', { user_id: 'some-uuid' })
8. Using Custom Schemas with Supabase API
By default, tables you create in the Supabase SQL Editor reside in the public
schema. For better organization, namespacing, or permission management, you might want to use custom PostgreSQL schemas.
Creating a Custom Schema:
CREATE SCHEMA private_schema;
Creating Tables in a Custom Schema:
CREATE TABLE private_schema.sensitive_data (
id serial primary key,
payload jsonb
);
Accessing Tables in Custom Schemas via API:
Supabase's PostgREST layer automatically detects tables in schemas other than public
.
- REST API: The API endpoints remain the same (
/rest/v1/table_name
), but PostgREST exposes tables from other schemas by default. You might need to manage access via roles and grants in PostgreSQL if you want fine-grained schema-level access control beyond standard RLS. If there's a name collision (same table name inpublic
and another schema), you might need specific configuration or use RPC. Check the PostgREST documentation for handling schema visibility if needed. - Client Libraries: The client libraries work seamlessly. You simply reference the table name as usual:
// Accessing a table in 'private_schema' (assuming RLS/permissions allow)
const { data, error } = await supabase
.from('sensitive_data') // No need to prefix with schema name here
.select('*')
.eq('id', 1);
Supabase handles mapping the table name to the correct schema behind the scenes. Ensure your RLS policies correctly reference tables if they involve cross-schema queries or functions.
Using custom schemas is a standard PostgreSQL practice that Supabase fully supports, allowing for more structured database organization.
9. Conclusion
The Supabase API offers a remarkably efficient way to build applications by providing instant, secure, and scalable access to your PostgreSQL database. From the auto-generated REST endpoints and helpful client libraries to the robust security provided by Row Level Security and the extensibility offered by Edge Functions, Supabase empowers developers to focus on building features rather than boilerplate backend infrastructure.
By understanding the core concepts – API keys, RLS, the REST structure, type generation, and custom functions – you can effectively leverage the full power of the Supabase platform. Remember to prioritize security
Want an integrated, All-in-One platform for your Developer Team to work together with maximum productivity?
Apidog delivers all your demans, and replaces Postman at a much more affordable price!
