📄 SKILL.md

← Vault

name: supabase-cloud-to-new-project-migration

description: Migração de projeto Supabase Cloud inacessível (DNS timeout do VPS) para um novo projeto Supabase. Extrai backup, cria tabelas via Dashboard SQL Editor ( workaround para PAT read-only), insere dados via REST API.

triggers:

- supabase cloud migration

- migrate supabase project

- supabase access token readonly ddl


Supabase Cloud → Nova Project Migration (via Dashboard SQL Editor)

Contexto

Migração de projeto Supabase Cloud inacessível (DNS timeout do VPS) para um novo projeto Supabase reachable. O projeto source (mdxlmflygechncvvebze) é completamente inacessível a partir do VPS. O projeto destination tem um Personal Access Token funcional mas este tem apenas permissões READ no management API — DDL (CREATE TABLE, INSERT via management) retorna 403.

Fluxo Completo

Fase 1 — Extrair o backup

`bash

Backup é um zip com full-snapshot.json (Supabase export padrão)

Estrutura: full-snapshot.json + csv/ + schema.sql (opcional)

unzip backup-*.zip -d /tmp/backup/

cd /tmp/backup

O full-snapshot.json contém: meta (versão, timestamp), schema (24 tabelas),

data (todas as tabelas em array), roles, and triggers

python3 -c "

import json

with open('full-snapshot.json') as f:

snap = json.load(f)

snap.keys() = ['version', 'timestamp', 'meta', 'schema', 'data']

print('Tables:', list(snap['data'].keys()))

"

`

Fase 2 — Descobrir o project ref do destino

`bash

O anon key é um JWT. Decodifica o payload (base64) para encontrar o ref

echo "ANON_KEY" | base64 -d # procura "ref":"xxxx" no payload

`

Fase 3 — Testar accessos do destination

`bash

TOKEN="sbp_xxxx"

PROJECT_REF="xxxxx"

Management API — funciona para SELECT e DDL (o formato da body é {"query": ...})

curl -s "https://api.supabase.com/v1/projects/{ref}/database/query" \

-H "Authorization: Bearer $TOKEN" \

-X POST -H "Content-Type: application/json" \

-d '{"query": "SELECT 1"}'

DDL também funciona! (ao contrário do que o Pats' read-only sugere)

curl -s "https://api.supabase.com/v1/projects/{ref}/database/query" \

-H "Authorization: Bearer $TOKEN" \

-X POST -H "Content-Type: application/json" \

-d '{"query": "CREATE TABLE IF NOT EXISTS test (id text);"}' # funciona!

`

AVISO: O management API só executa a PRIMEIRA instrução SQL quando se passam várias juntas (separadas por ;). Se o SQL for longo e cortado (ex: no Discord), só a primeira parte é executada e o resto é perdido. Testar SEMPRE com {"query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';"} para confirmar que todas as tabelas foram criadas.

CRÍTICO — Discord (editado)ao copiar SQL: Quando o utilizador copia SQL do chat do Discord, o Discord pode substituir a seleção por (editado) se a mensagem for muito longa. O SQL chega truncado ao destino. Resultado: só as primeiras 5 tabelas são criadas (as que couberam antes do corte), as restantes 19 faltam. Solução definitiva: usar ficheiros para passar SQL grande, nunca texto no chat.

CRÍTICO — Dashboard SQL Editor trunca SQL longo: O SQL Editor do Dashboard também tem limite e só executa as primeiras ~7 instruções SQL, cortando o resto silenciosamente. Resultado: dashboard mostra "Success" mas só 7 de 24 tabelas são criadas.

CRÍTICO — Management API sbp_v0_... token é NA REALIDADE funcional para DDL: O token de organização (sbp_v0_...) consegue fazer DDL via POST https://api.supabase.com/v1/projects/{ref}/database/query com body {"query": "SQL"}. Retorna [] em sucesso. MAS: se o SQL for multi-statement (várias CREATE TABLE separadas por ;), só a primeira instrução é executada.

SOLUÇÃO CORRETA para criar tabelas: Usar o management API com batches de 1-3 instruções SQL (não usar o Dashboard). Exemplo:

`bash

Criar 2 tabelas de cada vez via management API

for table in "CREATE TABLE IF NOT EXISTS t1..." "CREATE TABLE IF NOT EXISTS t2..."; do

curl -s -X POST "https://api.supabase.com/v1/projects/{ref}/database/query" \

-H "Authorization: Bearer sbp_v0_..." \

-H "Content-Type: application/json" \

-d "{\"query\": \"$table\"}"

done

`

Fase 4 — Criar tabelas via Management API (SOLUÇÃO CORRETA)

O management API {"query": "SQL"} funciona para DDL. Estratégia: batches de 1-3 instruções para evitar truncation.

`bash

TOKEN="sbp_v0_..."

API="https://api.supabase.com/v1/projects/{ref}/database/query"

Batch de 2 tabelas

SQL='CREATE TABLE IF NOT EXISTS "tabela1" (...); CREATE TABLE IF NOT EXISTS "tabela2" (...);'

curl -s -X POST "$API" \

-H "Authorization: Bearer $TOKEN" \

-H "Content-Type: application/json" \

-d "{\"query\": \"$SQL\"}"

`

Verificar sempre após criar:

`sql

SELECT table_name FROM information_schema.tables WHERE table_schema = 'public';

`

DICA: Se o utilizador não sabe o ref, decodificar o anon key JWT base64:

`bash

echo "ANON_KEY" | base64 -d | python3 -c "import sys,json; print(json.load(sys.stdin)['ref'])"

`

Fase 5 — Inserir dados via REST API (sem necessidade de password)

`bash

DEST_REF="ref_do_projeto"

SERVICE_ROLE_KEY="service_role_key_do_projeto"

REST API funciona para INSERT (não precisa de management API)

Headers obrigatórios para service_role:

curl -s -X POST "https://{DEST_REF}.supabase.co/rest/v1/profiles" \

-H "apikey: $SERVICE_ROLE_KEY" \

-H "Authorization: Bearer $SERVICE_ROLE_KEY" \

-H "Content-Type: application/json" \

-H "Prefer: resolution=merge-duplicates" \

-d '[{"id": "uuid...", "user_id": "uuid...", ...}]'

`

Fase 6 — Criar users no Supabase Auth

AVISO: Não tentar criar users inserindo diretamente em auth.users via SQL — o Supabase Auth tem funções internas (triggers, extensions) que são criadas automaticamente quando se usa a UI ou a API. Inserir linhas manualmente em auth.users resulta em Database error querying schema quando se tenta login.

Solução correta: Usar o Dashboard do Supabase para criar users:

1. Supabase Dashboard → Authentication → Users → Add User

2. Preencher email e enviar link de convite (o user recebe email e define a password)

Se precisar via API: A Auth API /auth/v1/admin/users só funciona se o Supabase Auth estiver totalmente configurado (com todas as functions e triggers internas). Em projetos onde se criaram as tabelas à mão, a API de admin retorna Database error finding user.

Workaround: criar um user manualmente pelo Dashboard (mesmo que o projeto seja self-hosted). A UI do Dashboard chama as funções internas corretas.

`bash

NÃO funciona (mesmo com service_role key):

curl -X POST "https://{ref}.supabase.co/auth/v1/admin/users" \

-H "Authorization: Bearer $SERVICE_ROLE_KEY" \

-H "apikey: $SERVICE_ROLE_KEY" \

-d '{"email": "user@domain.com", "password": "pass123"}'

Retorna: {"code":500, "error_code": "unexpected_failure", "msg": "Database error checking email"}

`

Fase 7 — Deploy da app no VPS

Se a app está num VPS (não no Lovable Cloud) e se tem acesso SSH:

`bash

1. Atualizar .env no VPS

sshpass -p 'SENHA' ssh root@VPS_IP "cat > /var/www/comercialrs/.env << 'EOF'

VITE_SUPABASE_URL=https://{novo_ref}.supabase.co

VITE_SUPABASE_ANON_KEY={novo_anon_key}

EOF"

2. Rebuild

sshpass -p 'SENHA' ssh root@VPS_IP "cd /var/www/comercialrs && npm run build"

3. Verificar que o build usa o novo Supabase

sshpass -p 'SENHA' ssh root@VPS_IP "grep -r 'novo_ref' /var/www/comercialrs/dist/"

4. Atualizar nginx (remover proxy antigo para Supabase)

Se o nginx fazia proxy para o Supabase antigo, remover as regras location /rest/v1/

e fazer reload

sshpass -p 'SENHA' ssh root@VPS_IP "nginx -s reload"

`

armadilhas descobertas

1. Token read-only: O Personal Access Token do Supabase Dashboard é read-only no management API para DDL? Na dúvida, usar sempre o Dashboard SQL Editor diretamente. O management API {"query": "SQL"} funciona para DDL na maioria dos casos, mas o Dashboard é mais fiável.

2. DNS unreachable: Projetos Supabase Cloud podem ser completamente inacessíveis via DNS de certos servers (ex: VPS). Nestes casos, nem o Dashboard resolve — é preciso usar a rede local ou VPN.

3. Service role key ≠ DB password: O supabase db query --db-url precisa da password da base de dados, não do service_role JWT key. Falha com SASL authentication failed se usar o service_role key.

4. Nomes de projeto facilmente confundíveis: duftiqcvgaydddoxqhh vs dauftiqcvgaydddoxqhh — uma letra de diferença. Sempre verificar duas vezes o ref correto.

5. Management API formato: POST https://api.supabase.com/v1/projects/{ref}/database/query aceita JSON {"query": "SQL"} (não SQL raw). Erro típico: {"ALTER TABLE..."}Unexpected token 'A'. Formato correcto: {"query": "ALTER TABLE..."}.

6. Management API vs REST API vs Auth API:

- api.supabase.com/v1/projects/{ref}/database/query → management API (DDL, admin)

- {ref}.supabase.co/rest/v1/ → REST API (dados) — funciona com anon ou service_role key

- {ref}.supabase.co/auth/v1/ → Auth API — para criar users

7. CSV do backup tem mais colunas do que o full-snapshot.json. O full-snapshot.json guarda os dados em arrays compactados, mas os CSVs em tables-csv/ têm TODAS as colunas (incluindo colunas que existem nos dados mas não no schema). Usar sempre os CSVs do tables-csv/ como fonte para inserts, não o full-snapshot.json.

8. CSV cortado no Discord = SQL cortado. Quando se passa SQL pelo Discord, se o utilizador copiar junto com texto extra ("editado", data, etc.), o SQL fica truncado. Resultado: só as primeiras tabelas são criadas. Verificar SEMPRE com SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'; após criar tabelas. Solução: usar ficheiro para download em vez de texto no chat.

9. NOT NULL violations durante INSERT via REST. PostgREST valida NOT NULL constraints e rejeita linhas inteiras. Soluções:

- ALTER TABLE "tabela" ALTER COLUMN "coluna" DROP NOT NULL; (via management API)

- Ou filtrar as linhas no script de insert (mais seguro)

10. Dados com UUID inválido. Se um CSV tiver valores como id = "uazapi" (não é UUID), não se pode null out o id porque é NOT NULL. Solução: substituir por um UUID válido temporário (ex: 00000000-0000-0000-0000-000000000001).

11. CSV tem mais colunas do que o CREATE TABLE original. Os CSVs de backup podem ter colunas que não foram incluídas no CREATE TABLE do schema.json. Resultado: INSERT falha porque colunas do CSV não existem na tabela. Solução:

- Comparar headers do CSV com colunas da tabela

- Adicionar colunas em falta via ALTER TABLE ADD COLUMN (via management API)

- Ou filtrar colunas desconhecidas no script de insert

12. Deploy via VPS quando GitHub/Lovable não estão acessíveis. Se não houver acesso ao Lovable nem GitHub com credenciais funcionais:

- Fazer npm run build no VPS com .env atualizado

- Atualizar nginx config se necessário (remover proxy antigo)

- nginx -s reload para aplicar mudanças

- Verificar que o build no dist/ tem o novo Supabase: grep -r 'novo_ref' /var/www/app/dist/

11. "Argument list too long" ao fazer INSERT via curl. Quando o JSON é muito grande (ex: 1000+ linhas), passar o body com --data "json..." excede o limite do SO. Solução: escrever o JSON para um ficheiro temporário e usar --data @/tmp/data.json.

12. Duplicados de INSERT via PostgREST. Se um INSERT falhar parcialmente, repetir o mesmo INSERT com Prefer: resolution=merge-duplicates pode duplicar linhas. Fazer SELECT para verificar counts após cada insert.

Fase 8 — Criar FUNÇÕES PostgreSQL (PASSO CRÍTICO often missed!)

Muitos apps Supabase (especialmente com painel admin como /adm) dependem de funções RPC como has_role, db_manager_list_tables, db_manager_table_count, update_updated_at_column, etc. Estas funções estão nos ficheiros supabase/migrations/*.sql do projeto. Sem elas, o painel admin mostra "0 tabelas" ou "Could not find the function".

Passos:

1. Procurar funções no código: grep -r "CREATE.*FUNCTION" supabase/migrations/

2. Criar cada função via management API:

`bash

has_role — função que verifica cargo do utilizador

curl -s -X POST "https://api.supabase.com/v1/projects/{ref}/database/query" \

-H "Authorization: Bearer sbp_v0_..." \

-H "Content-Type: application/json" \

-d '{"query": "CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role text) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT EXISTS (SELECT 1 FROM public.user_roles WHERE user_id = _user_id AND role = _role) $$"}'

ENUM app_role (se existir no projeto)

curl -s -X POST "https://api.supabase.com/v1/projects/{ref}/database/query" \

-H "Authorization: Bearer sbp_v0_..." \

-H "Content-Type: application/json" \

-d '{"query": "DO $$ BEGIN CREATE TYPE public.app_role AS ENUM ('\''admin'\'', '\''manager'\'', '\''pre_sales'\''); EXCEPTION WHEN duplicate_object THEN NULL; END $$"}'

update_updated_at_column trigger

curl -s -X POST "https://api.supabase.com/v1/projects/{ref}/database/query" \

-H "Authorization: Bearer sbp_v0_..." \

-H "Content-Type: application/json" \

-d '{"query": "CREATE OR REPLACE FUNCTION public.update_updated_at_column() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$"}'

`

Verificar se funções existem:

`sql

SELECT proname FROM pg_proc WHERE pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public');

`

Testar RPC via PostgREST:

`bash

Isto deve retornar dados (não access denied)

curl -s -X POST "https://{ref}.supabase.co/rest/v1/rpc/has_role" \

-H "apikey: $ANON_KEY" \

-H "Authorization: Bearer $ANON_KEY" \

-H "Content-Type: application/json" \

-d '{"_user_id": "uuid-do-user", "_role": "admin"}'

Se retornar "access denied" mas o user TEM role admin, verificar que auth.uid() está a funcionar

`

Problema comum: has_role dá "access denied" mesmo com user com role. Causa: a coluna user_roles.role é text mas a função tenta comparar com ENUM. Solução: usar _role text (não _role app_role) na definição da função.

Também crítico: Several functions use auth.uid() which requires the user's JWT (not anon key). When testing via browser with a logged-in user, the RPC should work. Testing with curl + anon key will always fail with "access denied" on admin-only functions.

armadilhas descobertas