Supabase REST API — Direct HTTP Access to Your Database
In this tutorial, you will learn about Supabase REST API. We cover key concepts, practical examples, and best practices to help you master this topic.
The Supabase REST API is an auto-generated HTTP API powered by PostgREST that lets you query, filter, and mutate your PostgreSQL database using standard HTTP requests without any SDK.
What You'll Learn
By the end of this lesson you will construct REST API requests to query and mutate data, use query parameters for filtering, sorting, and pagination, handle authentication headers, and understand the response format.
Why It Matters
The REST API enables any language or tool to interact with Supabase. It is essential for server-to-server communication, curl-based testing, scripting, and working with languages without an official SDK.
Real-World Use
DodaZIP uses the REST API for automated testing and CI/CD pipelines. Python scripts and shell scripts interact with the database via curl and HTTP requests without loading the full SDK.
flowchart LR
A[curl] -->|HTTP GET| API[PostgREST]
B[Python requests] -->|HTTP POST| API
C[CI/CD Pipeline] -->|HTTP PATCH| API
API -->|SQL| DB[(PostgreSQL)]
style API fill:#3ecf8e,color:#fff
Making Requests
Every Supabase REST API request includes the project URL, the table name, and authentication headers.
# Base URL format
# https://<project-ref>.supabase.co/rest/v1/<table>
# Query all rows
curl "https://example.supabase.co/rest/v1/files" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# Query with filter
curl "https://example.supabase.co/rest/v1/files?user_id=eq.user_abc" \
-H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# rest_queries.py
# Supabase REST API with Python requests
import requests
import os
def rest_api_examples():
url = os.getenv("SUPABASE_URL", "https://example.supabase.co")
anon_key = os.getenv("SUPABASE_ANON_KEY", "your-anon-key")
headers = {
"apikey": anon_key,
"Authorization": f"Bearer {anon_key}",
"Content-Type": "application/json",
}
# GET all files
response = requests.get(
f"{url}/rest/v1/files",
headers=headers
)
print(f"GET /files: {response.status_code}")
print(f"Results: {len(response.json())} rows")
# GET with filter
response = requests.get(
f"{url}/rest/v1/files",
headers=headers,
params={"user_id": "eq.user_abc", "select": "id,name,size_bytes"}
)
print(f"Filtered: {response.json()}")
rest_api_examples()
Query Parameters
Filter, sort, and paginate using query parameters.
# Filter operators
# eq (equals), neq (not equal), gt (greater than)
# lt (less than), gte (greater or equal), lte (less or equal)
# like, ilike (case-insensitive like), in (in list)
# is (is null), not.is (is not null)
# Examples
curl ".../files?status=eq.completed"
curl ".../files?size_bytes=gt.1000000"
curl ".../files?name=like.%report%"
# Sorting
curl ".../files?order=created_at.desc"
# Pagination
curl ".../files?offset=0&limit=20"
# Range headers
curl ".../files" -H "Range: 0-19"
# Counting
curl ".../files?select=count"
# query_params.py
# Advanced query parameters
def query_examples():
base = "https://example.supabase.co/rest/v1/files"
headers = {
"apikey": "your-anon-key",
"Authorization": "Bearer your-jwt",
}
queries = [
("All files", {}),
("Completed files", {"status": "eq.completed"}),
("Large files", {"size_bytes": "gt.10000000"}),
("Recent files", {"order": "created_at.desc"}),
("First page", {"offset": "0", "limit": "20"}),
("Specific columns", {"select": "id,name,created_at"}),
("Name search", {"name": "like.%report%"}),
]
print("REST API Query Examples:")
for desc, params in queries:
param_str = "&".join(f"{k}={v}" for k, v in params.items())
full_url = f"{base}?{param_str}" if param_str else base
print(f" {desc:20s} {full_url}")
query_examples()
Mutating Data
Insert, update, and delete via HTTP methods.
# rest_mutations.py
# Create, update, and delete via REST API
def rest_mutations():
url = "https://example.supabase.co/rest/v1/files"
headers = {
"apikey": "your-anon-key",
"Authorization": "Bearer your-jwt",
"Content-Type": "application/json",
"Prefer": "return=representation",
}
# INSERT a new row
new_file = {
"user_id": "user_abc",
"name": "new-report.pdf",
"size_bytes": 512000,
"mime_type": "application/pdf",
}
response = requests.post(url, headers=headers, json=new_file)
print(f"INSERT: {response.status_code}")
print(f"Created: {response.json()}")
# UPDATE matching rows
response = requests.patch(
url,
headers=headers,
params={"id": "eq.file_123"},
json={"status": "archived"}
)
print(f"UPDATE: {response.status_code}")
print(f"Updated: {response.json()}")
# DELETE matching rows
response = requests.delete(
url,
headers=headers,
params={"id": "eq.file_456"}
)
print(f"DELETE: {response.status_code}")
rest_mutations()
Authentication Headers
Understanding the different authentication modes.
# Using anon key (respects RLS)
curl -H "apikey: YOUR_ANON_KEY" \
-H "Authorization: Bearer USER_JWT"
# Using service_role key (bypasses RLS)
curl -H "apikey: YOUR_SERVICE_ROLE_KEY" \
-H "Authorization: Bearer YOUR_SERVICE_ROLE_KEY"
# Public access (no auth)
# Only works for tables where RLS allows public access
curl -H "apikey: YOUR_ANON_KEY"
# auth_headers.py
# Authentication modes
def auth_modes():
modes = {
"Anon Key + User JWT": "Client-side requests. Respects RLS policies. Users see only their data.",
"Service Role Key": "Server-side requests. Bypasses RLS. Full database access.",
"Anon Key Only": "Public requests. Only works if RLS allows SELECT for anonymous users.",
}
print("Authentication Modes:")
for mode, desc in modes.items():
print(f" {mode:30s} | {desc}")
auth_modes()
Common Mistakes
Not including the apikey header: The API key is required for every request, even anonymous ones.
Using service_role key from the browser: The service_role key bypasses RLS. Exposing it in client-side code is a critical security vulnerability.
Incorrect filter syntax: Filters use the format
column=operator.value. For example,status=eq.completednotstatus=completed.Forgetting the Prefer header for INSERT return: Without
Prefer: return=representation, the response does not include the created row.Mixing HTTP methods: Use GET for reads, POST for inserts, PATCH for updates, DELETE for deletes. Using GET for mutations will fail.
Practice Questions
What is the base URL format for the Supabase REST API?
https://<project-ref>.supabase.co/rest/v1/<table_name>.How do you filter rows where status equals completed? Add query parameter
?status=eq.completed.What HTTP method do you use to insert a row? POST.
What is the purpose of the Prefer header? It controls response behavior, such as
return=representationto return the inserted/updated row.Challenge: Write a bash script using curl that connects to a Supabase project, queries a table with filters and pagination, and formats the output as JSON.
FAQ
Mini Project
Create a Python script that uses the Supabase REST API to perform a complete CRUD workflow: query files, insert a new file, update its status, and delete it, with proper error handling at each step.
import requests
import json
def rest_crud_workflow():
base = "https://example.supabase.co/rest/v1/files"
headers = {"apikey": "your-key", "Authorization": "Bearer your-token", "Content-Type": "application/json"}
# Create
resp = requests.post(base, headers=headers, json={"name": "test.txt", "user_id": "user_abc", "size_bytes": 1000})
new_id = resp.json()[0]["id"]
print(f"Created: {new_id}")
# Read
resp = requests.get(f"{base}?id=eq.{new_id}", headers=headers)
print(f"Read: {resp.json()}")
# Update
requests.patch(f"{base}?id=eq.{new_id}", headers=headers, json={"name": "renamed.txt"})
print(f"Updated: {new_id}")
# Delete
requests.delete(f"{base}?id=eq.{new_id}", headers=headers)
print(f"Deleted: {new_id}")
rest_crud_workflow()
What's Next
Next: Supabase GraphQL for Graphql queries.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro