RLS is latency-sensitive with FDW
When using Foreign Data Wrappers in RLS policies, RLS becomes latency-sensitive due to extra database calls. Use the query plan analyzer to measure execution times and ensure queries are within expected ranges. For enterprise applications, contact enterprise@supabase.io for optimization guidance.
Built-in PostgREST retries enabled by default in supabase-js v2.102.0+
Starting with supabase-js v2.102.0, PostgREST queries using .from() and .rpc() include built-in automatic retries for transient errors. Retries are enabled by default and use exponential backoff with jitter.
Retryable HTTP status codes for PostgREST
PostgREST automatic retries trigger on HTTP status codes 408 (Request Timeout), 409 (Conflict), 503 (Service Unavailable), and 504 (Gateway Timeout), as well as network failures.
Only idempotent methods retried in PostgREST
Only idempotent HTTP methods (GET, HEAD, OPTIONS) and POST requests used by PostgREST are retried. Other HTTP methods are not automatically retried.
Disable PostgREST built-in retries
To disable built-in PostgREST retries, pass the option db.retry: false when creating the Supabase client. Use this if you prefer to handle retries yourself.
Disable PostgREST retries configuration example
The following example shows how to disable built-in retries: import { createClient } from '@supabase/supabase-js'; const supabase = createClient('https://your-project-id.supabase.co', 'your-publishable-key', { db: { retry: false, }, });
Use fetch-retry for non-PostgREST retries
The fetch-retry package allows you to add retries to non-PostgREST requests such as auth, storage, and functions. It wraps the native fetch function and applies retry logic to all requests made by the Supabase client.
Install fetch-retry and supabase-js
To use fetch-retry with Supabase, install both packages: npm install @supabase/supabase-js fetch-retry
Configure fetch-retry with Supabase client
To integrate fetch-retry with Supabase, wrap the fetch function and pass it to createClient: import { createClient } from '@supabase/supabase-js'; import fetchRetry from 'fetch-retry'; const fetchWithRetry = fetchRetry(fetch); const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...', { global: { fetch: fetchWithRetry, }, });
fetch-retry configuration options
fetch-retry accepts configuration options including: retries (number of retry attempts), retryDelay (function returning delay in milliseconds), and retryOn (array of HTTP status codes or custom function to determine whether to retry). Example: const fetchWithRetry = fetchRetry(fetch, { retries: 3, retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), retryOn: [520], });
fetch-retry exponential backoff with cap
A common fetch-retry retryDelay strategy is exponential backoff with a maximum delay: (attempt) => Math.min(1000 * 2 ** attempt, 30000). This doubles the delay with each attempt, starting at 1 second on the first retry and capping at 30 seconds.
Custom retryOn function in fetch-retry
The retryOn option in fetch-retry can accept a custom function with signature (attempt, error, response) => boolean. This allows inspecting the attempt number, error, and response to decide whether to retry. Example: retryOn: (attempt, error, response) => { const shouldRetry = (attempt, error, response) => attempt < 3 && response && response.status == 520 && response.url.includes('rpc/your_database_function'); if (shouldRetry(attempt, error, response)) { console.log(`Retrying request... Attempt #${attempt}`, response); return true; } return false; }
Warning: High retry counts risk connection pool exhaustion
Enabling retries with a high number of attempts can exhaust the Data API connection pool, resulting in lower throughput and failed requests. Only enable retries for network errors like Cloudflare 520 status codes.
Use built-in retries for most PostgREST cases
For most use cases, the built-in PostgREST retry mechanism is sufficient. Use fetch-retry only when you need retries on non-PostgREST requests or need fine-grained control over retry behavior.
error.hint contains actionable fixes from Postgres
The error.hint field on a PostgrestError contains the exact fix Postgres provides. For example, permission-denied errors include the literal GRANT SQL statement to run. Always check error.hint first before reading other fields.
Log full error object, not just error.message
When logging errors from supabase-js calls, log the complete error object rather than only error.message. Logging only the message field hides important information like hints and details that are necessary for debugging.
supabase-js returns { data, error } pair, never throws
Every supabase-js call returns an object containing data and error fields as a { data, error } pair instead of throwing exceptions. Check the error field to handle failures.
PostgrestError fields by usefulness order
PostgrestError contains four fields in order of usefulness: hint (always check first for actionable fix), code (use for branching logic as it is stable across versions), details (contains offending value or key when hint and message are not enough), and message (human summary, less useful for debugging).
Branch on error.code, not error.message
For programmatic branching, use error.code instead of error.message because codes are stable across Postgres and PostgREST versions, while message text changes between versions.
PostgrestError example: permission denied with GRANT hint
A permission-denied error on a table with revoked GRANT from anon role produces: code 42501, message 'permission denied for table users', and hint containing the exact GRANT statement needed to fix it.
Recommended error handling pattern for database calls
The recommended pattern is: destructure { data, error } from the response, check if error exists, log the whole error object, and return early. This applies to database calls like select, insert, update, upsert, delete, and rpc.
AuthError fields: code and status
AuthError exposes error.code (e.g. 'invalid_credentials', 'email_not_confirmed') and error.status. Branch on code and log the whole error object.
StorageError fields: statusCode and name
StorageError exposes error.statusCode as an HTTP status string and a structured error name field (e.g. 'Duplicate', 'NotFound').
Edge Functions errors have three subclasses
Edge Functions errors are one of three subclasses: FunctionsHttpError, FunctionsFetchError, or FunctionsRelayError. Narrow with instanceof. For FunctionsHttpError, parse error.context.json() to get the function's own error payload.
Realtime subscribe callback receives status and err
The supabase.channel().subscribe() callback receives a status parameter and an err argument on failure. Log the whole err object as its cause field often holds the underlying reason.
Database call error handling example
Example: const { data, error } = await supabase.from('users').select(); if (error) { console.error(error); return; }
Auth error handling example
Example: const { data, error } = await supabase.auth.signInWithPassword({ email: 'example@email.com', password: 'exa••••••rd' }); if (error) { console.error(error); return; }
Storage error handling example
Example: const { data, error } = await supabase.storage.from('avatars').upload('public/avatar1.png', avatarFile); if (error) { console.error(error); return; }
Edge Functions error handling example with instanceof
Example showing how to handle Edge Functions errors: import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from '@supabase/supabase-js'; const { data, error } = await supabase.functions.invoke('hello'); if (error instanceof FunctionsHttpError) { console.error('Function error', await error.context.json()); } else if (error) { console.error(error); }
Realtime error handling example
Example: supabase.channel('room1').subscribe((status, err) => { if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') { console.error(status, err); } });
Audit logs stored in external log storage and optional Postgres database
Audit logs are stored in external log storage by default, which is cost-efficient and accessible through the dashboard. Optionally, logs can be stored in the Postgres database in the auth.audit_log_entries table, which is searchable via SQL but uses additional database storage. You can enable or disable Postgres database storage to optimize costs.
How to configure audit log storage in Supabase dashboard
To configure audit log storage: 1) Navigate to your project's dashboard. 2) Go to Authentication. 3) Find the Audit Logs under the Configuration section. 4) Toggle 'Write audit logs to the database' on to enable or off to disable database storage.
Audit log entry format and fields
Audit log entries contain a timestamp (ISO 8601 format), user_id (UUID), action (string), ip_address, user_agent, and metadata object. Example: {"timestamp": "2025-08-01T10:30:00Z", "user_id": "uuid", "action": "user_signedup", "ip_address": "192.168.1.1", "user_agent": "Mozilla/5.0...", "metadata": {"provider": "email"}}
Complete reference of audit log actions
Supabase auth audit logs track the following actions: login (user login attempt), logout (user logout), invite_accepted (team invitation accepted), user_signedup (new user registration), user_invited (user invitation sent), user_deleted (user account deleted), user_modified (user profile updated), user_recovery_requested (password reset request), user_reauthenticate_requested (user reauthentication required), user_confirmation_requested (email/phone confirmation requested), user_repeated_signup (duplicate signup attempt), user_updated_password (password change completed), token_revoked (refresh token revoked), token_refreshed (refresh token used to obtain new tokens), generate_recovery_codes (MFA recovery codes generated), factor_in_progress (MFA factor enrollment started), factor_unenrolled (MFA factor removed), challenge_created (MFA challenge initiated), verification_attempted (MFA verification attempt), factor_deleted (MFA factor deleted), recovery_codes_deleted (MFA recovery codes deleted), factor_updated (MFA factor settings updated), mfa_code_login (login with MFA code), and identity_unlinked (an identity unlinked from account).
Audit logs limitations
Auth audit logs have the following limitations: there may be a short delay before logs appear, and query capabilities are limited to the dashboard interface.