name: lovable-vps-migration
description: Migrar app React do Lovable Cloud para VPS quando Lovable está com instabilidade (ERR_ABORTED)
category: web-development
Lovable → VPS Migration (React/Vite)
Migrar um app do Lovable Cloud para VPS quando o Lovable está com instabilidade.
Quando Usar
- - Lovable Cloud com erros
net::ERR_ABORTEDem/rest/v1/*— mesmo problema vai ocorrer no VPS se o Supabase Cloud continuar instável - - App funciona mas dados não carregam (problema de infraestrutura Lovable, não do código)
- - Queres autonomia total do Lovable Cloud
- - Lovable está lento/instável e queres migrar temporária ou permanentemente
- - Importante:
ERR_ABORTEDé erro genérico de rede — pode vir do Lovable (bloqueado por firewall) OU do Supabase Cloud (timeout) - - Output: só
dist/client/assets/( chunks JS/CSS) - - Sem
dist/server/→ NÃO serve SSR no VPS - - Usa
server.mjswrapper simples ou nginx como proxy - - Problema: não funciona em Node.js padrão (só Workers)
- - Output:
dist/client/+dist/server/server.js(fetch handler SSR) - - Funciona no VPS com wrapper HTTP Node
- - ✅ Este é o que funciona no VPS
- - ⚠️ Mas pode usar
@lovable.dev/cloud-auth-jspara OAuth (ver secção OAuth) - -
/auth/v1/token?grant_type=password→ timeout - -
/rest/v1/?limit=1→ funciona ( requer service_role key) - -
/rest/v1/qualquer_tabela→ timeout (parece rate limiting ou bloqueado do VPS) - - Se o Supabase Cloud funciona do browser do utilizador (testar:
https://mdxlmflygechncvvebze.supabase.co/auth/v1/health→ responde mesmo sem apikey) - - → O React app deve conectar-se diretamente ao Supabase Cloud
- - → nginx NÃO deve ter
location /auth/v1/nemlocation /rest/v1/com proxy_pass - - Apenas quando o Supabase Cloud também não funciona do browser do utilizador
- - Ou quando se quer usar auth local + dados cloud (neste caso, ambos os problemas existem)
- - App carrega mas login fica em "loading" para sempre (o proxy nginx está a interceptar e a mandar para containers locais com base vazia)
- -
curl -v /rest/v1/membrosdo VPS →relation "public.membros" does not exist(o PostgREST local não tem as tabelas) - - Remover os
location /auth/v1/elocation /rest/v1/do nginx - - O app React vai diretamente ao Supabase Cloud (como fazia no Lovable)
- -
/auth/v1/token?grant_type=password→ timeout (sempre) - -
/rest/v1/qualquer_tabela→ timeout (do VPS) - -
/auth/v1/health→ "No API key found" (do browser do utilizador = funciona) - - Testar sempre do browser do utilizador, não só do VPS/terminal
- - Se o browser chega ao Supabase Cloud mas o VPS não → o problema é rota de rede, não o Supabase
- - Se o browser também não chega → o Supabase Cloud está em baixo ou banido
- -
deploy-vps-auth-1— GoTrue na porta 9999 (não expõe/auth/v1/*diretamente) - -
deploy-vps-kong-1— Kong API gateway na porta 8000/8001 (rota o tráfego) - - Se o VPS consegue resolver
mdxlmflygechncvvebze.supabase.co→ proxy nginx funciona - - Erro "no live upstreams while connecting to upstream" → VPS não consegue resolver DNS do Supabase Cloud
- - NÃO adicionar proxy
/rest/v1/no nginx - - No
.env, usarVITE_SUPABASE_URL=https://mdxlmflygechncvvebze.supabase.co(cloud direto) - - O browser resolve o DNS e acede diretamente ao Supabase Cloud
- - Apenas o
/auth/v1/passa pelo Kong local (auth local) - -
GOTRUE_SITE_URL— DEVE ser o domínio final (e.g.https://comercialrs.rochasalesseguros.com.br), não o IP nem outro domínio - -
GOTRUE_URI— URI interno do GoTrue (e.g.http://localhost:9999) - -
API_EXTERNAL_URL— URL externa (mesmo valor que GOTRUE_SITE_URL) - -
GOTRUE_JWT_SECRET— secret partilhado com PostgREST (verificar que é igual aoPGRST_JWT_SECRET) - - A anon key é o mesmo JWT que o auth local usa para assinar tokens
- - Ou seja, o anon key do
.envdo frontend é o mesmo do Supabase Cloud (já que usa o mesmo JWT_SECRET) - - Verificar: se o PostgREST valida JWTs, a anon key do frontend tem de ser a mesma que assina os tokens no auth local
- - rayca.soares@rochasalesseguros.com.br
- - maria.giulha@rochasalesseguros.com.br
- - vinicius.sales@rochasalesseguros.com.br
- - pietra.maniezzo@rochasalesseguros.com.br
- - davi.freire@rochasalesseguros.com.br
- -
npm run buildfalha com:You are using Node.js 18.20.8. Vite requires Node.js version 20.19+ or 22.12+. Please upgrade your Node.js version. - - Fix: usar nvm:
nvm use 20 && npm run build(nvm available at/root/.nvm/versions/node/v20.20.2/bin/) - - Ou criar alias no
.bashrcou usar path completo no script de build - -
/var/www/documentos2/app/— onde o systemd service aponta, temnode_modulesestart-server.mjsoriginal - -
/var/www/documentos/deploy-vps-nodejs/app/— onde o build completo (103 assets) foi gerado, masnode_modulesfoi apagado (cache clearing), não temstart-server.mjs
⚠️ Dois Tipos de Build Lovable — Abordagem Diferente
Cloudflare Worker (build "Cloudflare Workers" no Lovable):
Node.js / Node Server (build "Node" no Lovable):
Como distinguir: ver package.json:
`json
"scripts": { "start": "node .output/server/index.mjs" } // → Node Server
`
Se "start" usa Cloudflare Workers API: export default { fetch } → não serve no VPS.
Se o build gera dist/server/server.js e o package.json tem "type": "module" → é Node Server target.
Fluxo Geral
`
Lovable (front) ──[ERR_ABORTED]──✗──→ Supabase Cloud
↓ (exportar ZIP)
VPS (nginx) ────────────────✓──→ Supabase Cloud (mesmo projeto, dados intactos)
`
Passo a Passo
1. Exportar do Lovable
1. Vai ao projeto no Lovable
2. Download como ZIP
3. Envia o ZIP para a VPS via scp ou para o agent
2. Transferir para VPS
`bash
Upload
scp comercialrs-main.zip root@VPS:/tmp/comercialrs.zip
SSH
ssh root@VPS
`
3. Extrair (sem unzip?)
Se unzip não existe na VPS, usa Python:
`python
import zipfile, shutil
z = zipfile.ZipFile("/tmp/comercialrs.zip")
z.extractall("/tmp")
shutil.move("/tmp/comercialrs-main", "/var/www/comercialrs")
`
4. Build (Node Server target — TanStack Start)
`bash
cd /var/www/APP_DIR
npm install
⚠️ DEPENDÊNCIA CRÍTICA: @lovable.dev/cloud-auth-js
Lovable adiciona esta lib automaticamente mas NÃO a inclui no ZIP
npm install @lovable.dev/cloud-auth-js
npm run build
Output: dist/server/ (SSR handler) + dist/client/ (assets)
`
4b. Ajustar estrutura de output (.output/ vs dist/)
O package.json normalmente espera .output/server/index.mjs mas o build gera dist/server/server.js. Ajustar:
`bash
mkdir -p .output/server .output/client
cp -r dist/server/* .output/server/
cp -r dist/client/* .output/client/
mv .output/server/server.js .output/server/index.mjs
`
4c. Criar wrapper HTTP (server.js)
O index.mjs exports { fetch } (fetch handler h3), não um servidor HTTP. Precisa de wrapper:
`javascript
// server.js — na raiz do projeto
import { createServer } from 'node:http';
import handler from './.output/server/index.mjs';
const PORT = process.env.PORT || 3000;
const HOST = process.env.HOST || '0.0.0.0';
const server = createServer((req, res) => {
const headers = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
if (v) headers.set(k, Array.isArray(v) ? v.join(', ') : v);
}
const fetchReq = new Request(http://${req.headers.host}${req.url}, {
method: req.method,
headers,
body: ['POST', 'PUT', 'PATCH'].includes(req.method) ? req : undefined,
});
handler.fetch(fetchReq) // ← .fetch() é um objeto, não função direta
.then((fetchRes) => {
res.statusCode = fetchRes.status;
for (const [k, v] of fetchRes.headers.entries()) res.setHeader(k, v);
if (fetchRes.body) {
fetchRes.body.pipeTo(new WritableStream({
write(chunk) { res.write(chunk); },
close() { res.end(); }
})).catch(() => res.end());
} else res.end();
})
.catch((err) => {
console.error('Handler error:', err);
res.statusCode = 500;
res.end('Internal Server Error');
});
});
server.listen(PORT, HOST, () => {
console.log(Server on http://${HOST}:${PORT});
});
`
Testar: PORT=3010 node server.js → curl http://127.0.0.1:3010/ → 200 HTML
5. nginx (React SPA)
`nginx
server {
listen 80;
listen [::]:80;
server_name teu-dominio.com;
root /var/www/comercialrs/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Opcional: proxy para API backend
# location /api/ {
# proxy_pass http://127.0.0.1:8001;
# }
}
`
6. SSL (LetsEncrypt)
`bash
certbot --nginx -d teu-dominio.com
`
Armadilhas Conhecidas
@lovable.dev/cloud-auth-js — OAuth que depende do Lovable Cloud
Sintoma: App carrega mas fica em tela preta ou login não funciona — mesmo no VPS.
Esta lib faz OAuth através dos servidores Lovable Cloud. No VPS ela não conecta e trava o RequireAuth (fica em loading infinito).
Solução: Substituir src/integrations/lovable/index.ts por Supabase direto:
`typescript
// src/integrations/lovable/index.ts — substituir por:
import { supabase } from "@/integrations/supabase/client";
export const lovable = {
auth: {
signInWithOAuth: async (provider: "google" | "apple" | "microsoft" | "lovable", opts?: { redirect_uri?: string }) => {
if (provider !== "google") {
return { error: new Error(Provider ${provider} not supported on VPS. Use Google.) };
}
const { data, error } = await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: opts?.redirect_uri || window.location.origin + "/auth/callback",
},
});
if (error) return { error };
if (data.url) {
window.location.href = data.url;
return { redirected: true };
}
return { redirected: false };
},
},
};
`
E criar uma rota /auth/callback:
`typescript
// src/routes/auth.callback.tsx
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { Loader2 } from "lucide-react";
import { supabase } from "@/integrations/supabase/client";
export const Route = createFileRoute("/auth/callback")({
component: AuthCallback,
});
function AuthCallback() {
const navigate = useNavigate();
const [error, setError] = useState
useEffect(() => {
const handleCallback = async () => {
const params = new URLSearchParams(window.location.search);
const errorParam = params.get("error");
if (errorParam) {
setError(params.get("error_description") || errorParam);
return;
}
// ⚠️ MUST call exchangeCodeForSession — getSession() alone does NOT work!
// Supabase OAuth sends ?code=... which must be exchanged for a session
const code = params.get("code");
if (code) {
const { error: sessionError } = await supabase.auth.exchangeCodeForSession(code);
if (sessionError) {
setError(sessionError.message);
return;
}
} else {
const { error: sessionError } = await supabase.auth.getSession();
if (sessionError) {
setError(sessionError.message);
return;
}
}
navigate({ to: "/" });
};
handleCallback();
}, [navigate]);
if (error) {
return (
Erro no login
{error}
);
}
return (
Entrando…
);
}
`
⚠️ Ponto crítico: exchangeCodeForSession(code) é OBRIGATÓRIO. getSession() apenas lê a sessão do storage — NÃO faz a troca do código OAuth. Sem isto, o utilizador faz login mas não fica com sessão.
Fluxo OAuth completo:
1. signInWithOAuth({ provider: "google", options: { redirectTo: "https://teu-dominio.com/auth/callback" } })
2. Browser abre Google → User faz login
3. Google redireciona para https://teu-dominio.com/auth/callback?code=XXX
4. exchangeCodeForSession(code) troca code por sessão
5. navigate({ to: "/" }) vai para home
No Supabase Dashboard:确认 Google OAuth provider配置正确,redirect URI包含 /auth/callback。
Supabase Cloud — timeout no Cloud Auth vs REST API
O Supabase Cloud pode ter apenas o Auth a falhar (timeout em /auth/v1/) mas o REST API funcionar (/rest/v1/). Isso foi confirmado neste projeto:
Sintoma: Login funciona mas dados não carregam → mesmo problema que o Lovable original.
Armadilha CRÍTICA: Não adicionar proxies nginx quando o browser chega ao Supabase Cloud
Quando NÃO adicionar proxies nginx (/auth/v1/ ou /rest/v1/):
Quando ADICIONAR proxies nginx (Arquitectura Híbrida):
Sintoma de ter adicionado proxies erradamente:
Como corrigir:
Conectividade intermitente do VPS ao Supabase Cloud
O VPS pode ter conectividade intermitente ao Supabase Cloud:
O mesmo Supabase Cloud funciona do browser do utilizador mas não do VPS. Isto indica um problema de rota de rede específico entre o VPS e o Supabase Cloud, não um problema do Supabase Cloud em si.
Implicação para debug:
Teste rápido no browser: https://mdxlmflygechncvvebze.supabase.co/auth/v1/health
Responde com {"message":"No API key..."} = cloud funciona.
CRÍTICO: Arquitetura de containers — Kong é o gateway, não a porta 9999
A porta 9999 é o auth GoTrue diretamente, MAS o routing de API é pelo Kong.
Containers Supabase local stack:
- Porta 8000: expõe o auth (/auth/v1/* → GoTrue na 9999)
- Porta 8001: expõe o REST (/rest/v1/* → PostgREST na 3002)
O problema real发现的: Fazer curl http://127.0.0.1:9999/auth/v1/token → 404. O endpoint /auth/v1/ só existe através do Kong na porta 8000.
Teste correto do auth local:
`bash
Este funciona (passa pelo Kong):
curl -s -X POST "http://127.0.0.1:8000/auth/v1/token?grant_type=password" \
-H "Content-Type: application/json" \
-H "apikey:
-d '{"email":"user@test.com","password":"pass"}'
→ {"access_token":"...","token_type":"bearer",...}
Este NÃO funciona (porta 9999 não tem /auth/v1/):
curl "http://127.0.0.1:9999/auth/v1/health"
→ 404 page not found
Mas isto funciona (root path do GoTrue):
curl "http://127.0.0.1:9999/health"
→ {"name":"GoTrue","description":"GoTrue is a user registration and authentication API"}
`
nginx proxy DEVE apontar para a porta 8000 (Kong), não 9999:
`nginx
location /auth/v1/ {
proxy_pass http://127.0.0.1:8000/auth/v1/; # ← Kong, não 9999!
...
}
`
Armadilha CRÍTICA: TanStack Start createServerFn só funciona em route files
Sintoma: POST /_serverFn retorna 500 com "Server function info not found" — mesmo depois de fazer build.
Causa: O TanStack Start v1.x Vite plugin (@tanstack/start/vite) só detecta createServerFn() chamadas em ficheiros de rota (ficheiros que usam createFileRoute ou estão dentro de src/routes/). Se criares server functions num ficheiro normal como src/server/gmail.functions.ts, o plugin não detecta e o manifest fica vazio {}.
Como confirmar:
`bash
Verificar se o manifest está vazio
cat dist/server/assets/_tanstack-start-manifest_v-*.js
Se mostra só {} ou [] → as server functions não foram detectadas
`
Solução 1 — Bypass completo (recomendado para desvincular do Lovable):
Não usar createServerFn nem o sistema de manifest. Em vez disso:
1. Criar um route file vazio em src/routes/api/gmail/_serverFn.ts (para o TanStack registar a rota)
2. No server.js, intercetar POST /api/gmail/_serverFn antes de passar ao TanStack handler
3. Implementar toda a lógica diretamente no server.js (sem createServerFn)
`javascript
// server.js — interceptar antes do TanStack
const HANDLED_PATHS = ['/api/gmail/_serverFn'];
const server = createServer((req, res) => {
const pathname = new URL(req.url, 'http://localhost').pathname;
if (HANDLED_PATHS.includes(pathname) && req.method === 'POST') {
// Ler body
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const { __fn, data } = JSON.parse(body);
// Router manual para cada função
if (__fn === 'getGmailConnection') return handleGetGmailConnection(req, res, data);
if (__fn === 'sendGmailWithAttachments') return handleSendGmail(req, res, data);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: Unknown function: ${__fn} } }));
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: e.message } }));
}
});
return;
}
// Passar ao TanStack handler para as outras rotas
// ...
});
`
Solução 2 — Fix no ficheiro de rota:
Mover as server functions do ficheiro .ts normal para um route file:
`typescript
// src/routes/api/gmail/_serverFn.ts — CERTO: está em routes
import { createServerFn } from '@tanstack/react-router';
export const getGmailConnection = createServerFn({ method: 'GET' })
.handler(async () => { / ... / });
`
Nota: Se o objetivo é desvinccular do Lovable, a Solução 1 é mais robusta — não depende do sistema de build/plugin do TanStack.
Arquitectura Híbrida: Auth Local + Base de Dados Cloud
Se o Supabase Cloud Auth está instável mas os dados estão intactos, a solução é:
`
App no VPS ──/auth/v1/*──→ Kong (8000) ──→ Auth Local (9999)
──/rest/v1/*──→ Supabase Cloud direto (browser resolve DNS)
`
Nota: O /rest/v1/ NÃO deve ir pelo Kong/PostgREST local se a DB cloud estiver inacessível do VPS.
O browser faz diretamente https://mdxlmflygechncvvebze.supabase.co/rest/v1/* (o browser resolve o DNS, o VPS não precisa).
Quando o proxy nginx para /rest/v1/ funciona:
Solução quando VPS não resolve DNS do Supabase Cloud:
nginx: proxy para auth local (via Kong) e REST direto
`nginx
Proxy /auth/v1/* → Kong (porta 8000) → GoTrue (porta 9999)
location /auth/v1/ {
proxy_pass http://127.0.0.1:8000/auth/v1/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
proxy_buffering off;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
NÃO adicionar proxy /rest/v1/* se Supabase Cloud DNS não resolve do VPS!
O browser chama https://mdxlmflygechncvvebze.supabase.co/rest/v1/* diretamente.
Se quiseres proxy (só quando funciona do VPS):
location /rest/v1/ {
proxy_pass https://mdxlmflygechncvvebze.supabase.co/rest/v1/;
proxy_set_header Host mdxlmflygechncvvebze.supabase.co;
...
}
`
Configurar Auth Local (deploy-vps-auth-1)
Verificar variáveis de ambiente:
`bash
docker inspect deploy-vps-auth-1 --format '{{range .Config.Env}}{{println .}}{{end}}'
`
Variáveis críticas:
Para alterar GOTRUE_SITE_URL:
`bash
Opção 1: editar .env e recriar
docker exec deploy-vps-auth-1 env | grep -i site
Opção 2: recriar o container com novo env
docker stop deploy-vps-auth-1
docker rm deploy-vps-auth-1
(recriar com docker-compose ou docker run com GOTRUE_SITE_URL=novo_dominio)
`
Chave API: anon key do PostgREST local
O VITE_SUPABASE_ANON_KEY no frontend não é a mesma do Supabase Cloud. Deve ser a chave pública do PostgREST local.
Se o PostgREST usa AUTHENTICATOR=anon com JWT validation:
Se não funciona: testar criar user manualmente no auth local e fazer login via curl:
`bash
Através do Kong (porta 8000) — FUNCIONA:
curl -s -X POST "http://127.0.0.1:8000/auth/v1/token?grant_type=password" \
-H "Content-Type: application/json" \
-H "apikey:
-d '{"email":"user@test.com","password":"pass"}'
Diretamente na porta 9999 — NÃO FUNCIONA para /auth/v1/:
curl -s -X POST "http://127.0.0.1:9999/auth/v1/token?grant_type=password"
→ 404 page not found
`
Migração de Utilizadores
Se não se consegue aceder ao Supabase Cloud Dashboard (mesmo ERR_ABORTED):
1. Obter lista de emails dos utilizadores (pedir ao utilizador ou extraer do código fonte)
2. Criar cada utilizador no auth local via API (através do Kong na porta 8000)
3. Cada user recebe link de reset password via email (se SMTP configurado)
4. Alternativa sem SMTP: criar users com email_confirm:true + password definida pelo admin
Criar user no auth local:
`bash
curl -s -X POST "http://127.0.0.1:8000/auth/v1/signup" \
-H "Content-Type: application/json" \
-H "apikey:
-d '{"email":"user@exemplo.com","password":"TempPass123!","email_confirm":true}'
`
5 users criados para ComercialRS (exemplo real):
Se SMTP não está configurado, os users precisam de usar "Esqueci a password" num ambiente onde o email possa ser enviado, ou um admin reseta a password via API:
`bash
Obter user_id primeiro
curl -s "http://127.0.0.1:8000/auth/v1/admin/users" \
-H "Authorization: Bearer
Reset password
curl -s -X PUT "http://127.0.0.1:8000/auth/v1/admin/users/
-H "Authorization: Bearer
-H "Content-Type: application/json" \
-d '{"password":"NovaPassword123!"}'
`
Verificar conectividade à base de dados Supabase Cloud
`bash
Testar REST API root (funciona mesmo sem auth)
curl https://mdxlmflygechncvvebze.supabase.co/rest/v1/?limit=1 \
-H "apikey: TUASERVICE_ROLE_KEY" \
-H "Authorization: Bearer TUASERVICE_ROLE_KEY"
Testar tabelas específicas (pode falhar com rate limiting)
curl "https://mdxlmflygechncvvebze.supabase.co/rest/v1/reunioes?limit=1" \
-H "apikey: TUASERVICE_ROLE_KEY" \
-H "Authorization: Bearer TUASERVICE_ROLE_KEY"
`
Armadilha CRÍTICA: Build usa Node.js 20, VPS default é Node 18
O Vite (usado pelo TanStack Start) requer Node.js 20+. Se o VPS tiver Node 18 como default:
Se o diretório local de trabalho for apagado (e.g. /tmp/comercialrs), todas as sessões SSH falham:
`
cd: /tmp/comercialrs: No such file or directory
`
Fix: Usar workdir="/tmp" no parâmetro do terminal.
unzip não existe na VPS
Muitos VPS não têm unzip. Alternativa Python (sempre disponível):
`python
import zipfile, shutil, os
z = zipfile.ZipFile("/tmp/comercialrs.zip")
z.extractall("/tmp")
shutil.rmtree("/var/www/comercialrs", ignore_errors=True)
shutil.move("/tmp/comercialrs-main", "/var/www/comercialrs")
os.remove("/tmp/comercialrs.zip")
`
Projeto pode ser React+Vite (não Next.js)
Nem todo projeto Lovable é Next.js. Se o ZIP contém vite.config.ts e package.json com "dev": "vite" → é React+Vite, não Next.js.
Build: npm run build (não next build). Output vai para dist/, não .next/.
Verificação
1. Build completa sem erros → npm run build
2. dist/index.html existe
3. nginx serve na porta 80
4. Abrir domínio → app carrega
5. Login funciona (pode precisar de reconfigurar OAuth providers no Supabase Dashboard)
Teste Rápido do Auth Local
`bash
Criar user de teste
curl -s -X POST http://127.0.0.1:9999/signup \
-H "Content-Type: application/json" \
-d '{"email":"teste@exemplo.com","password":"Teste123!","email_confirm":true}'
Login
curl -s -X POST http://127.0.0.1:9999/token?grant_type=password \
-H "Content-Type: application/json" \
-d '{"email":"teste@exemplo.com","password":"Teste123!"}'
Deve retornar {"access_token": "...", "token_type": "bearer", ...}
`
Se o signup/login funciona via curl mas o browser não: o problema está no frontend (provavelmente @lovable.dev/cloud-auth-js).
Diagnóstico de Problemas
| Sintoma | Causa provável | Solução |
| --------- | --------------- | --------- |
| Login fica em loading forever | @lovable.dev/cloud-auth-js a chamar cloud auth | Substituir por @supabase/supabase-js |
| 401 após login | JWT secret mismatch entre auth e PostgREST | Verificar GOTRUE_JWT_SECRET = PGRST_JWT_SECRET |
| Redirect URI mismatch | GOTRUE_SITE_URL errado | Atualizar para o domínio final |
| Dados não carregam após login | PostgREST não alcança DB cloud | Testar curl do VPS ao Supabase Cloud REST |
| CORS error no browser | nginx não permite cross-origin | Adicionar headers CORS no location proxy |
Browser ainda chama /~oauth/initiate após rebuild | Cache do browser保留了 ficheiros JS antigos | Limpar cache do site no browser (DevTools → Application → Storage → Clear site data) |
| Login funciona no terminal mas não no browser | Sessão não foi trocada pelo código OAuth | Verificar que exchangeCodeForSession(code) é chamado no callback, não só getSession() |