The "PGRST117: HTTP verb not allowed" error occurs when you attempt to use an HTTP method (GET, POST, PUT, DELETE, etc.) that is not permitted for a specific Supabase API endpoint or database resource. This typically happens due to Row Level Security (RLS) policies, table permissions, or incorrect API usage.
The "PGRST117: HTTP verb not allowed" error is a PostgREST error code that indicates the HTTP method you're trying to use is not permitted for the requested resource. PostgREST is the REST API layer that Supabase uses to expose your PostgreSQL database as a RESTful API. When you make an HTTP request to a Supabase endpoint (like `/rest/v1/table_name`), PostgREST checks whether the HTTP verb (GET, POST, PUT, PATCH, DELETE) is allowed for that specific resource based on: 1. **Row Level Security (RLS) policies** - Policies that control access to rows in your tables 2. **Database permissions** - PostgreSQL GRANT/REVOKE permissions on tables and functions 3. **API endpoint configuration** - Some endpoints may only support specific HTTP methods This error commonly occurs when: - Trying to POST to a table where INSERT permissions are not granted - Attempting to DELETE records without proper DELETE permissions - Using PATCH or PUT on resources that are read-only - Making requests to endpoints that don't support the HTTP method you're using
First, verify that RLS is enabled and properly configured for your table:
-- Check if RLS is enabled on the table
SELECT tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND tablename = 'your_table_name';
-- View existing RLS policies
SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public' AND tablename = 'your_table_name';Common issues:
- RLS is enabled but no policies exist (blocks all operations)
- Policies don't include the HTTP method you're trying to use
- Policy conditions are too restrictive
To fix, create or modify policies:
-- Example: Allow INSERT for authenticated users
CREATE POLICY "Users can insert their own data" ON your_table_name
FOR INSERT WITH CHECK (auth.uid() = user_id);
-- Example: Allow UPDATE for authenticated users
CREATE POLICY "Users can update their own data" ON your_table_name
FOR UPDATE USING (auth.uid() = user_id);
-- Example: Allow DELETE for authenticated users
CREATE POLICY "Users can delete their own data" ON your_table_name
FOR DELETE USING (auth.uid() = user_id);Check that the database role used by Supabase has the necessary permissions:
-- Check privileges for the anon role (public access)
SELECT grantee, privilege_type, table_name
FROM information_schema.role_table_grants
WHERE grantee = 'anon' AND table_schema = 'public';
-- Check privileges for the authenticated role
SELECT grantee, privilege_type, table_name
FROM information_schema.role_table_grants
WHERE grantee = 'authenticated' AND table_schema = 'public';
-- Check privileges for the service_role (admin)
SELECT grantee, privilege_type, table_name
FROM information_schema.role_table_grants
WHERE grantee = 'service_role' AND table_schema = 'public';If permissions are missing, grant them:
-- Grant INSERT, UPDATE, DELETE permissions to authenticated role
GRANT INSERT, UPDATE, DELETE ON your_table_name TO authenticated;
-- Grant all permissions to service_role (use cautiously)
GRANT ALL PRIVILEGES ON your_table_name TO service_role;
-- For anon role (public access, use with caution)
GRANT INSERT, UPDATE, DELETE ON your_table_name TO anon;Note: Be careful when granting permissions to the 'anon' role as it allows unauthenticated access.
Ensure you're using the appropriate HTTP method for your operation:
| Operation | HTTP Method | Supabase Client Example |
|-----------|-------------|-------------------------|
| Read data | GET | supabase.from('table').select() |
| Insert data | POST | supabase.from('table').insert() |
| Update data | PATCH | supabase.from('table').update() |
| Upsert data | POST/PATCH | supabase.from('table').upsert() |
| Delete data | DELETE | supabase.from('table').delete() |
Common mistakes:
- Using PUT instead of PATCH for updates
- Using POST for updates instead of PATCH
- Using GET for operations that modify data
Verify your client code:
// Correct: Using PATCH for updates
const { error } = await supabase
.from('profiles')
.update({ username: 'new_username' })
.eq('id', user.id);
// Incorrect: Using POST for updates (would cause PGRST117)
const { error } = await supabase
.from('profiles')
.post({ username: 'new_username' }) // post() doesn't exist
.eq('id', user.id);Supabase provides different API keys with different permission levels:
1. anon key - For public, unauthenticated access (limited permissions)
2. service_role key - For administrative operations (bypasses RLS)
3. User JWT - For authenticated user access (respects RLS policies)
Common issues:
- Using anon key for operations that require authentication
- Using service_role key in client-side code (security risk)
- Expired or invalid JWT tokens
Verify your Supabase client initialization:
// Client-side (browser) - use anon key
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key' // Public key
)
// Server-side - can use service_role key (keep it secret!)
const supabaseAdmin = createClient(
'https://your-project.supabase.co',
'your-service-role-key', // Keep this server-side only!
{
auth: {
autoRefreshToken: false,
persistSession: false
}
}
)For authenticated operations, ensure users are signed in:
// First, sign in the user
const { data: { user }, error: authError } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'password'
});
// Then make authenticated requests
if (user) {
const { data, error } = await supabase
.from('protected_table')
.insert({ user_id: user.id, data: 'some data' });
}Test your API calls with different authentication states to isolate the issue:
// Test 1: Unauthenticated request (anon key)
async function testUnauthenticated() {
const { data, error } = await supabase
.from('test_table')
.insert({ test: 'value' });
console.log('Unauthenticated result:', { data, error });
}
// Test 2: Authenticated request (user JWT)
async function testAuthenticated() {
// Sign in first
const { data: { session }, error: signInError } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'testpassword'
});
if (session) {
const { data, error } = await supabase
.from('test_table')
.insert({ user_id: session.user.id, test: 'value' });
console.log('Authenticated result:', { data, error });
}
}
// Test 3: Service role request (admin, bypasses RLS)
async function testServiceRole() {
const supabaseAdmin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);
const { data, error } = await supabaseAdmin
.from('test_table')
.insert({ test: 'value' });
console.log('Service role result:', { data, error });
}Interpretation:
- If all tests fail: Database permissions issue
- If only unauthenticated fails: RLS policy issue
- If authenticated fails but service_role works: RLS policy too restrictive
- If all work: Client code issue (wrong HTTP method, etc.)
Some database objects may be read-only:
-- Check if it's a view instead of a table
SELECT table_name, table_type
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'your_table_name';
-- Check if table has triggers or rules preventing modifications
SELECT tgname, tgtype, tgrelid::regclass
FROM pg_trigger
WHERE tgrelid = 'your_table_name'::regclass;
-- Check for INSTEAD OF triggers on views
SELECT viewname, definition
FROM pg_views
WHERE schemaname = 'public'
AND viewname = 'your_table_name';Common read-only scenarios:
1. Materialized views - Cannot be directly modified
2. Views without INSTEAD OF triggers - Read-only by default
3. Foreign tables (FDW) - May have limited write support
4. Tables with BEFORE triggers that reject operations
Solutions:
- For views: Create INSTEAD OF triggers or modify the underlying tables
- For materialized views: Refresh them and modify source tables
- For foreign tables: Check FDW configuration for write support
## Understanding PostgREST HTTP Method Mapping
PostgREST maps HTTP methods to SQL operations as follows:
| HTTP Method | SQL Operation | Typical Use Case |
|-------------|---------------|------------------|
| GET | SELECT | Retrieve data, can be filtered/ordered |
| POST | INSERT | Create new records |
| PATCH | UPDATE | Modify existing records (partial update) |
| PUT | UPDATE | Replace entire resource (less common) |
| DELETE | DELETE | Remove records |
### RLS Policy Command Mapping
When creating RLS policies, you need to specify which commands they apply to:
-- Policy applies to SELECT (GET requests)
CREATE POLICY "policy_name" ON table_name
FOR SELECT USING (condition);
-- Policy applies to INSERT (POST requests)
CREATE POLICY "policy_name" ON table_name
FOR INSERT WITH CHECK (condition);
-- Policy applies to UPDATE (PATCH requests)
CREATE POLICY "policy_name" ON table_name
FOR UPDATE USING (condition) WITH CHECK (condition);
-- Policy applies to DELETE (DELETE requests)
CREATE POLICY "policy_name" ON table_name
FOR DELETE USING (condition);### Common Permission Patterns
1. Public read, authenticated write:
-- Allow anyone to read
CREATE POLICY "Public read access" ON table_name
FOR SELECT USING (true);
-- Only authenticated users can write
CREATE POLICY "Authenticated users can insert" ON table_name
FOR INSERT WITH CHECK (auth.role() = 'authenticated');
CREATE POLICY "Users can update own data" ON table_name
FOR UPDATE USING (auth.uid() = user_id);2. User-owned data:
-- Users can only access their own data
CREATE POLICY "User access own data" ON table_name
FOR ALL USING (auth.uid() = user_id);3. Team-based access:
-- Users can access team data
CREATE POLICY "Team member access" ON table_name
FOR ALL USING (
EXISTS (
SELECT 1 FROM team_members
WHERE team_id = table_name.team_id
AND user_id = auth.uid()
)
);### Debugging Tips
1. Enable detailed logging:
-- Set higher log level for PostgREST
ALTER DATABASE your_database SET log_statement = 'all';2. Check request headers:
curl -v -X POST https://your-project.supabase.co/rest/v1/table_name -H "Authorization: Bearer YOUR_JWT" -H "Content-Type: application/json" -d '{"data": "value"}'3. Test with pgAdmin or psql:
- Direct database access can help isolate API vs database issues
- Test SQL commands that correspond to your HTTP operations
### Performance Considerations
- Each RLS policy adds overhead to query execution
- Complex policy conditions can slow down queries
- Consider using security definer functions for complex access control
- Use indexes on columns referenced in policy conditions
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists