📄 SKILL.md

← Vault

name: supabase-cloud-rpc-bypass-rls

description: Inserir registros no Supabase Cloud quando JWT é rejeitado — usar função RPC SECURITY DEFINER via management API

triggers:

- "Supabase Cloud 401/403 Invalid API key ao usar service_role key no PostgREST"

- "Supabase Cloud RLS bloqueando inserts mesmo com role correta"

- "Supabase service_role JWT rejected by PostgREST /rest/v1/"

- "JWT ES256 token com sub UUID corrompido (37 chars ao invés de 36)"

- "Upload retornando 500 após correção de UUID no token"


Supabase Cloud: RPC SECURITY DEFINER Bypass para Inserções

Problema

No Supabase Cloud (não self-hosted), o JWT do service_role às vezes é rejeitado pelo PostgREST REST API com "Invalid API key", mesmo sendo um JWT válido com role: service_role. Isso impede inserções pelo servidor via service_role key.

Sintoma:

`

{"message":"Invalid API key","hint":"Double check your Supabase anon or service_role API key."}

`

Solução

Criar uma função RPC com SECURITY DEFINER que insere os dados diretamente no banco, ignorando RLS. Usar a management API (api.supabase.com) para criar a função — funciona mesmo quando o PostgREST rejeita o service_role key.

1. Criar a função via Management API

Endpoint: POST https://api.supabase.com/v1/projects/{ref}/database/query

Auth: Authorization: Bearer {PAT} (Personal Access Token do Supabase Dashboard)

SQL:

`sql

CREATE OR REPLACE FUNCTION public.insert_document(

p_client_id uuid,

p_uploaded_by uuid,

p_category text,

p_original_name text,

p_stored_name text,

p_storage_path text,

p_mime_type text,

p_size_bytes bigint

) RETURNS uuid

LANGUAGE plpgsql

SECURITY DEFINER

SET search_path = public

AS $$

DECLARE

new_id uuid;

BEGIN

INSERT INTO public.documents (

id, client_id, title, uploaded_by, category, original_name, stored_name,

storage_path, mime_type, size_bytes, created_at, updated_at

) VALUES (

gen_random_uuid(), p_client_id, COALESCE(p_original_name, 'Documento'), p_uploaded_by, p_category, p_original_name, p_stored_name,

p_storage_path, p_mime_type, p_size_bytes, now(), now()

) RETURNING id INTO new_id;

RETURN new_id;

END;

$$;

`

2. Chamar do servidor (upload-document.ts)

`typescript

const insertRes = await fetch(${SB_URL}/rest/v1/rpc/insert_document, {

method: "POST",

headers: {

"Content-Type": "application/json",

apikey: SB_ANON_KEY, // usa ANON key

Authorization: Bearer ${token}, // token do usuário logado

},

body: JSON.stringify({

p_client_id: clientId,

p_uploaded_by: userId,

p_category: category,

p_original_name: originalName,

p_stored_name: storedName,

p_storage_path: storagePath,

p_mime_type: file.type,

p_size_bytes: file.size,

}),

});

`

A função é chamada com apikey: ANON_KEY + Authorization: Bearer {user_token}. O SECURITY DEFINER faz a função executar como postgres (superuser), ignorando RLS.

3. Obter o ID retornado

A função retorna o UUID do registro criado — resposta é o UUID direto (não array):

`typescript

const insertedId = await insertRes.json();

`

Notas Importantes

`typescript

// Direct SQL via Management API

const sqlRes = await fetch(

https://api.supabase.com/v1/projects/${SB_REF}/database/query,

{

method: 'POST',

headers: {

'Content-Type': 'application/json',

'Authorization': Bearer ${SB_PAT}, // PAT, not anon key

},

body: JSON.stringify({

query: `INSERT INTO public.documents (id, client_id, title, uploaded_by, ...)

VALUES (gen_random_uuid(), '${clientId}', '${title}', '${userId}', ...);`

}),

}

);

`