Default privileges on Supabase projects
On existing projects, tables created in the public schema receive SELECT, INSERT, UPDATE, and DELETE privileges for anon, authenticated, and service_role roles by default. Functions receive EXECUTE privileges. These grants make new objects reachable through the Data API even when you do not intend to expose them. Supabase is changing the platform default to revoke these automatic grants so that exposure becomes opt-in. These default privileges are part of the standard Supabase permission model and do not bypass RLS. The internal supabase_admin role grants them to anon, authenticated, and service_role, but supabase_admin cannot authenticate through the Data API.
Dedicated API schemas for security boundary
A dedicated schema such as api adds another boundary around your Data API. Objects in such a schema define the API surface, while internal tables and helper functions remain in schemas that are not exposed. You can control access with grants in any schema. A dedicated schema makes the exposed surface easier to identify and audit.
Pre-request checks for API security requirements
RLS policies do not cover every API security requirement. Add pre-request checks for requirements such as enforcing per-IP or per-user rate limits, checking custom or additional API keys before allowing further access, rejecting requests after exceeding a quota or requiring payment, and disallowing direct access to certain tables, views, or functions in exposed schemas. A Postgres pre-request function reads request information and performs these checks before serving a response.
Access request information in pre-request functions
Use the Postgres current_setting() function to access request information. Available settings: request.method (GET, HEAD, POST, PUT, PATCH, DELETE), request.path (table, view, or rpc/function path), request.headers (JSON object of request headers), request.cookies (JSON object of request cookies), request.jwt (JSON object of JWT payload). To access the client's IP address, look up the X-Forwarded-For header in request.headers and use split_part() to extract the client IP before the first comma.
Request information available via current_setting()
| current_setting() | Example | Description |
| --- | --- | --- |
| request.method | GET, HEAD, POST, PUT, PATCH, DELETE | Request's method |
| request.path | table | Table's path |
| request.path | view | View's path |
| request.path | rpc/function | Function's path |
| request.headers | { "User-Agent": "...", ... } | JSON object of the request's headers |
| request.cookies | { "cookieA": "...", "cookieB": "..." } | JSON object of the request's cookies |
| request.jwt | { "sub": "a7194ea3-...", ... } | JSON object of the JWT payload |
Raising HTTP error responses from pre-request functions
A pre-request function can raise an exception using raise sqlstate 'PGRST' to stop a request and return a custom HTTP response. The message parameter accepts a JSON object with keys: code, message, details, hint. The detail parameter accepts a JSON object with keys: status (HTTP status code), headers (JSON object of response headers), and status_text (custom status text for non-standard codes like 419). Use JSON functions and operators to build dynamic responses from exceptions.
PostgREST 42501 error: missing grant statement
If a required grant is missing, PostgREST returns a 42501 error with message 'permission denied for table your_table' and a hint that names the exact GRANT statement needed, for example: 'Grant the required privileges to the current role with: GRANT SELECT ON public.your_table TO anon;'
Revoke default privileges to make new objects opt-in for Data API
To prevent automatic grants on new objects in the public schema, run the following SQL statements: alter default privileges for role postgres in schema public revoke select, insert, update, delete on tables from anon, authenticated, service_role; alter default privileges for role postgres in schema public revoke execute on functions from anon, authenticated, service_role; alter default privileges for role postgres in schema public revoke usage, select on sequences from anon, authenticated, service_role; alter default privileges for role postgres in schema public revoke execute on functions from public;. After this, new tables, functions, and sequences require explicit grants before Data API roles can access them.
Enable Row Level Security on tables exposed via Data API
Enable RLS on every table and view exposed through the Data API. Tables created through the Supabase Dashboard have RLS enabled by default. Enable RLS explicitly for tables created in the SQL Editor or through another tool using: alter table your_table enable row level security;. With RLS enabled, create policies that control which data users can access and update based on their authentication token.
RLS does not apply to functions
RLS policies do not apply to functions. Grant EXECUTE only to the roles that need to call the functions. Review every SECURITY DEFINER function carefully to ensure it does not pose security risks.
Disable the Data API entirely in Dashboard
If your app never uses Supabase client libraries, REST, or GraphQL data endpoints, you can disable the Data API completely. In the Dashboard, go to Data API integration overview and turn 'Enable Data API' off. With the Data API disabled, none of the auto-generated REST endpoints respond, regardless of grants or RLS.
Create and register a pre-request function
Create a Postgres function to run checks before each Data API request. Create the function with: create function public.check_request() returns void language plpgsql security definer as $$ begin -- your logic here end; $$;. Then register it to run on every Data API request with: alter role authenticator set pgrst.db_pre_request = 'public.check_request';. Finally, reload the PostgREST configuration with: notify pgrst, 'reload config';. The function now runs before every Data API request.
Rate limit per IP using pre-request function
Create a private.rate_limits table to record IP address and timestamp of write requests: create table private.rate_limits (ip inet, request_at timestamp); create index rate_limits_ip_request_at_idx on private.rate_limits (ip, request_at desc);. Rate limiting only applies to POST, PUT, PATCH, and DELETE requests; GET and HEAD requests run in read-only mode and cannot be rate-limited. Example pre-request function that rejects requests with HTTP 420 when an IP makes more than 100 write requests in 5 minutes:
create function public.check_request() returns void language plpgsql security definer as $$
declare
req_method text := current_setting('request.method', true);
req_ip inet := split_part(current_setting('request.headers', true)::json->>'x-forwarded-for', ',', 1)::inet;
count_in_five_mins integer;
begin
if req_method = 'GET' or req_method = 'HEAD' or req_method is null then
return;
end if;
select count(*) into count_in_five_mins from private.rate_limits where ip = req_ip and request_at between now() - interval '5 minutes' and now();
if count_in_five_mins > 100 then
raise sqlstate 'PGRST' using message = json_build_object('message', 'Rate limit exceeded, try again after a while')::text, detail = json_build_object('status', 420, 'status_text', 'Enhance Your Calm')::text;
end if;
insert into private.rate_limits (ip, request_at) values (req_ip, now());
end;
$$;
Verify custom API keys in pre-request function
Create a private.anon_api_keys table to store application-managed API keys: create table private.anon_api_keys (id uuid primary key, -- other relevant fields);. Example pre-request function that checks for x-app-api-key header and blocks requests with HTTP 403 if the key is not registered when using the anon role:
create function public.check_request() returns void language plpgsql security definer as $$
declare
req_app_api_key text := current_setting('request.headers', true)::json->>'x-app-api-key';
is_app_api_key_registered boolean;
jwt_role text := current_setting('request.jwt.claims', true)::json->>'role';
begin
if jwt_role <> 'anon' then
return;
end if;
select true into is_app_api_key_registered from private.anon_api_keys where id = req_app_api_key::uuid limit 1;
if is_app_api_key_registered is true then
return;
end if;
raise sqlstate 'PGRST' using message = json_build_object('message', 'No registered API key found in x-app-api-key header.')::text, detail = json_build_object('status', 403)::text;
end;
$$;
Application-managed API keys for anon role
Use application-managed API keys when you need another access check. This approach applies to applications that use the Data API without RLS policies or do not use Supabase Auth and rely on the anon role. The apikey header is mandatory and not configurable. If you use another API key, distribute both the publishable key and your application's custom key. Application-managed API keys are stored in the private.anon_api_keys table and distributed independently.
SQL to REST translation example
Example SQL query that can be translated: select title, description from books where description ilike '%cheese%' order by title desc limit 5 offset 10
SQL to REST API Translator availability
Supabase provides a SQL to REST API Translator tool to help translate SQL queries to equivalent PostgREST requests and Supabase client code.
PostgREST supports a subset of SQL
PostgREST does not support all SQL queries. Not all SQL queries will translate to PostgREST requests.
cURL requests with custom schema using Accept-Profile header
For GET or HEAD requests to a custom schema, use the Accept-Profile header: curl '<SUPABASE_URL>/rest/v1/todos' -H "apikey: <SUPABASE_PUBLISHABLE_KEY>" -H "Authorization: Bearer <SUPABASE_PUBLISHABLE_KEY>" -H "Accept-Profile: myschema"
cURL requests with custom schema using Content-Profile header
For POST, PATCH, PUT and DELETE requests to a custom schema, use the Content-Profile header: curl -X POST '<SUPABASE_URL>/rest/v1/todos' -H "apikey: <SUPABASE_PUBLISHABLE_KEY>" -H "Authorization: Bearer <SUPABASE_PUBLISHABLE_KEY>" -H "Content-Type: application/json" -H "Content-Profile: myschema" -d '{"column_name": "value"}'
Publishable and secret keys cannot use Bearer authorization
You cannot send a publishable or secret key in the `Authorization: Bearer ...` header. If the value exactly equals the `apikey` header, your request will be forwarded to your project's database, but it will be rejected as the value is not a JWT.
Four types of API keys in Supabase
Supabase provides four types of API keys: Publishable keys (format `sb_publishable_...`, low privileges, safe to expose online in web pages, mobile/desktop apps, GitHub actions, CLIs, source code); Secret keys (format `sb_secret_...`, elevated privileges, use only in backend components like servers, Edge Functions, microservices); `anon` (JWT format, low privileges, legacy version of publishable keys, available on Platform and CLI); `service_role` (JWT format, elevated privileges, legacy version of secret keys, available on Platform and CLI).
API keys vs Supabase Auth responsibility
API keys answer the question 'What is accessing the project?' (identifying web pages, mobile apps, servers, Edge Functions). Supabase Auth answers the question 'Who is accessing the project?' (identifying individual users like Monica, Jian Yang, Gavin, etc.). API keys do not distinguish between users, only between applications.
Publishable key Postgres role mapping
When using a publishable key, if no user is logged in via Supabase Auth, the Postgres role used for RLS is `anon`. If a user is logged in via Supabase Auth, the Postgres role used for RLS is `authenticated`.
Secret key browser protection
Secret keys cannot be used in the browser. If a secret key is used in a browser (detected via User-Agent header match), it will always reply with HTTP 401 Unauthorized.
Secret keys bypass Row Level Security
Secret keys authorize access via the built-in `service_role` Postgres role, which has full access to the project's data. This role uses the `BYPASSRLS` attribute, skipping all Row Level Security policies. Secret keys should only be used in secure, developer-controlled components like servers, Edge Functions, microservices, periodic jobs, and admin tools.
Creating new API keys does not revoke legacy keys
Creating publishable and secret keys adds them alongside existing `anon` and `service_role` keys without affecting them. Legacy keys remain valid until explicitly disabled in the Settings > API Keys section of the Dashboard. This is a separate step from creating new keys.
Secret key exposure do's and don'ts
Do not add secret keys to web pages, public documents, source code, or bundle in executables for mobile, desktop, or CLI apps. Do not send over chat applications, email, or SMS. Never use in a browser, even on localhost. Do not pass in URLs or query parameters as these are often logged. Be careful passing them in request headers without prior log sanitization. Take extra care logging potentially invalid API keys as typos might reveal the real key. Never reveal, copy, use, or manipulate on hardware devices without full disk encryption that you do not directly own or control.
Best practices for secret key handling
Always work with secret keys on computers you fully own or control. Use secure and encrypted send tools (often provided by password managers) to share API keys, but prefer the Settings > API Keys section of the Dashboard instead. Prefer encrypting them when stored in files or environment variables. Do not add to source control, especially for CI scripts; use the tool's native secrets capability instead. Prefer using a separate secret key for each backend component. If you must include them in logs, log only the first few random characters (never more than 6). If you wish to log or store which valid API key was used, store it as a SHA256 hash.
Secret key rotation in Dashboard
To rotate a compromised secret key (`sb_secret_...`), use the Settings > API Keys section of the Dashboard to create a new secret API key, then replace the compromised key. Once all components are using the new key, delete the compromised one. Deleting a secret key is irreversible.
PostgREST converts Postgres to RESTful API
PostgREST is a standalone web server that turns a Postgres database directly into a RESTful API. Supabase uses it with the pg_graphql extension to provide a GraphQL API. Source code is at github.com/PostgREST/postgrest, written in Haskell, and licensed under MIT.
Envoy serves as API gateway
Envoy is a cloud-native, high-performance edge and service proxy used by Supabase as its API gateway. Source code is at github.com/envoyproxy/envoy, written in C++, and licensed under Apache 2.0.
Realtime manages WebSocket connections
Realtime is a scalable WebSocket engine for managing user Presence, broadcasting messages, and streaming database changes. Source code is at github.com/supabase/realtime, written in Elixir, and licensed under Apache 2.
Management API
Manage your Supabase projects programmatically via the Management API. This feature is generally available but not available on self-hosted deployments.
Auto-generated GraphQL API via pg_graphql
Supabase provides fast GraphQL APIs through a custom Postgres GraphQL extension called pg_graphql.
Auto-generated REST API via PostgREST
Supabase automatically generates RESTful APIs from your database schema without requiring any code.
Publishable key privileges and behavior
The publishable key carries the same low privileges as the anon key. Row Level Security policies behave identically with the publishable key. User authentication through Supabase Auth remains unchanged, with users signing in and receiving their own JWT.
Places to check for legacy key usage
Before deactivating legacy keys, check all places that hold Supabase keys including: mobile or desktop app versions already in users' hands, CI/CD pipelines and deployment scripts, third-party integrations and webhooks, cron jobs, workers, pg_net calls, and Database Webhooks.
Legacy key deactivation is reversible
Legacy keys can be deactivated in the Settings > API Keys section of the Dashboard after confirming nothing uses them. They can be re-activated if a client that still depends on them is discovered.
API key migration cannot use Authorization Bearer header
Publishable and secret keys cannot be sent in the Authorization: Bearer header. They must be sent on the apikey header instead.
Secret key protections and access control
Secret keys add protections that service_role keys do not have. They return HTTP 401 if used in a browser (detected via User-Agent header) and allow running separate keys per service so a single leaked key only requires rotating that one key. Secret keys bypass Row Level Security and have full access to data.
Legacy API key to new key mapping
The anon key is replaced by the publishable key. The service_role key is replaced by the secret key. Both key types work simultaneously during migration, allowing clients to be swapped one at a time before deactivating legacy keys.
Supabase integration points
Supabase provides four integration points: the Postgres connection (anything that works with Postgres also works with Supabase projects), the Project REST API and client libraries, the Project GraphQL API, and the Platform API.
Partner integration Method 2 redirect record endpoint signature
The Method 2 redirect record endpoint receives a signed JWT from Supabase and returns a one-time redirect URL. It should be hosted at a controlled URL and path without requiring authentication, but should apply rate limiting to prevent abuse. The endpoint accepts POST requests with Content-Type: application/json containing a request body with a token field holding the signed JWT.
Partner integration redirect method overview
Partners can implement partner integrations via OAuth using one of two redirect methods. Method 1 (Redirect) is easier to build but cannot verify that the incoming user was sent by Supabase. Method 2 (Signed redirect) is more work but cryptographically verifies that the redirect originated from Supabase and is recommended for production integrations.
Partner integration Method 1 redirect endpoint query parameters
When redirecting via Method 1, Supabase appends query parameters to the GET endpoint URL: project_id (Supabase project ref of the project where the user clicked Install Integration) and organization_slug (Supabase organization slug where the user clicked Install Integration). These should be saved and used to fetch project or organization details or pre-select them in UI after OAuth completion.
Partner integration Method 1 redirect flow steps
In Method 1, implement a GET endpoint at a controlled URL. Supabase redirects users to this endpoint with project_id and organization_slug parameters. The endpoint may ask users to sign up, sign in, or perform setup tasks. Once complete, immediately redirect to the Supabase authorization URL without further user interaction to start the OAuth flow.
Partner integration Method 2 redirect record response format
After successful JWT validation in Method 2, generate a UUID to identify the redirect record (also called an integration record), save it in your system with an expiry (typically 1 hour), and return a JSON response with three fields: integrationId (the UUID uniquely identifying the redirect record), redirectUrl (the URL the user will be redirected to, must contain the integrationId somewhere in its path), and expiresAt (the time when the redirect record expires, typically 1 hour from creation).
Partner integration Method 2 redirect handler endpoint
Implement a GET endpoint at the redirectUrl returned in the previous step, with the redirect record's UUID in its path. When a user arrives: (1) Extract the redirect record UUID from the URL path. (2) Look it up in your system; if it doesn't exist or has expired, return 401 Unauthorized. (3) Optionally walk the user through any required setup (signing up, signing in, or configuring your system). (4) Redirect the user to the Supabase authorization URL to start the OAuth flow.
Partner integration Method 2 setup flow design recommendation
If setup steps are needed on your site for Method 2, design the experience as a wizard that ends by redirecting to the Supabase authorization URL. This minimizes the chance of users getting distracted, navigating elsewhere on your site, and abandoning the OAuth flow.
Partner integration Method 2 configuration details to share with Supabase
When implementing Method 2, send Supabase the following: (1) The URL of the redirect record endpoint. (2) The URL pattern of the redirect handler endpoint. (3) The aud claim value you want Supabase to send in the signed JWT. Also request from Supabase the public keys and key IDs for both the staging and production environments.
Data API (PostgREST) errors troubleshooting table
Symptom: PGRST002/PGRST106; schema cache errors; could not find table/relationship; new column or table not recognized; 42P01; 520 errors; API returns nothing. Layer: Data API (PostgREST) → edge_logs, postgres_logs. Troubleshooting guides: Refresh schema cache; PGRST002; New objects not recognized; 42P01; 520 errors; API not returning.
Using User-Agent header for metadata
To attach additional metadata to a request, use the User-Agent header for device or version identification. Do not log Personal Identifiable Information (PII) in the User-Agent header to avoid infringing data protection privacy laws and prevent user fingerprinting.
API logs endpoint and metadata
API logs show all network requests and responses for REST and GraphQL APIs. When Read Replicas are enabled, logs are automatically filtered between databases and the API Load Balancer endpoint. The upstream database that handles a request can be found under the 'Redirect Identifier' field, equivalent to 'metadata.load_balancer_redirect_identifier' when querying underlying logs.
Allowed request headers in API logs
The following request headers are permitted in API logs: accept, cf-connecting-ip, cf-ipcountry, host, user-agent, x-forwarded-proto, referer, content-length, x-real-ip, x-client-info, x-forwarded-user-agent, range, prefer.
Allowed response headers in API logs
The following response headers are permitted in API logs: cf-cache-status, cf-ray, content-location, content-range, content-type, content-length, date, transfer-encoding, x-kong-proxy-latency, x-kong-upstream-latency, sb-gateway-mode, sb-gateway-version.
PostgREST report charts and metrics
The PostgREST report includes: Total Requests (HTTP requests to PostgREST endpoints, shows API usage alongside WebSocket activity), Response Errors (error rates with 4XX and 5XX status codes, shows API reliability and user experience issues including top routes), Response Speed (average response time for PostgREST requests, shows performance bottlenecks and optimization opportunities including top routes), Network Traffic (ingress and egress usage, shows data transfer costs and CDN effectiveness).
Viewing organization members with Management API
Organization members can be listed using the Supabase Management API endpoint: GET https://api.supabase.com/v1/organizations/{ORG_ID}/members with Authorization Bearer token header. Access token is obtained from https://supabase.com/dashboard/account/tokens.
Management API for backups
You can manage backups programmatically using the Management API at https://api.supabase.com/v1/projects/{PROJECT_REF}/database/backups. Endpoints include listing all available backups and restoring from PITR backups using the recovery_time_target_unix parameter.
Delete project via management API
To delete a project using the Management API, make a DELETE request to https://api.supabase.com/v1/projects/<project-ref> with an Authorization header containing a Bearer token: curl -X DELETE https://api.supabase.com/v1/projects/<project-ref> -H "Authorization: Bearer <access-token>"