Pular para o conteúdo
📦 SDK JS

@superdb/client e @superdb/auth-js

Dois SDKs: o @superdb/client, com os nomes de método do supabase-js, e o @superdb/auth-js, só com o auth.

Instalação

terminal
# Com os nomes do supabase-js (recomendado pra migrar)
npm install @superdb/client

# Só o auth, sem o cliente de dados
npm install @superdb/auth-js

@superdb/client

Segue os nomes do @supabase/supabase-js. Os módulos .auth, .from() e .storage funcionam, e o Accept-Profile do projeto é setado automaticamente. A sessão se renova sozinha. O .channel() (Realtime) ainda é stub; veja a seção Realtime abaixo.

💡

.from() funciona. Com o SDK, o Accept-Profile do projeto vai sozinho — não precisa mandar header manual. Se quiser bater no REST cru (sem SDK), o host é https://api.superdb.com.br/<tabela> (sem /rest/v1/), com Accept-Profile: proj_<slug>.

createClient

lib/db.ts
import { createClient } from '@superdb/client'

const db = createClient(
  process.env.NEXT_PUBLIC_SUPERDB_URL!,   // https://auth.superdb.com.br
  process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!,
  { project: 'meu_projeto' }              // obrigatório — vira o Accept-Profile
)

Auth (funciona)

auth.ts
// Cadastro e login com e-mail e senha
await db.auth.signUp({ email, password })
await db.auth.signInWithPassword({ email, password })

// Login social (PKCE): abra data.url; na volta, troque o ?code= pela sessão
const { data } = await db.auth.signInWithOAuth({ provider: 'google', redirectTo: 'https://meuapp.com.br/callback' })
await db.auth.exchangeCodeForSession(code)

// Sessão e usuário atual
const { data: { session } } = await db.auth.getSession()
const { data: { user } } = await db.auth.getUser()

// Reagir a login, saída e renovação (a sessão se renova sozinha antes de o token de 1 hora vencer)
const { data: { subscription } } = db.auth.onAuthStateChange((evento, sessao) => {
  // INITIAL_SESSION, SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED
})
subscription.unsubscribe() // ao sair da tela

// Metadados do usuário (mescla) e troca de senha
await db.auth.updateUser({ data: { nome: 'Ana' } })
await db.auth.updateUser({ password: novaSenha, current_password: senhaAtual })

// Esqueci a senha: o link volta para a sua tela com #token=... no endereço
await db.auth.resetPasswordForEmail(email, { redirectTo: 'https://meuapp.com.br/nova-senha' })
await db.auth.resetPassword({ token, password: novaSenha })

// Sair
await db.auth.signOut()
💡

A renovação é do SDK desde a versão 0.3. Ela acontece um minuto antes de o token vencer, uma vez só mesmo com várias abas abertas (reusar um refresh token derruba a sessão). Dá para desligar com createClient(url, anonKey, { project, autoRefreshToken: false }). Se o seu app usa o sessao.ts dos templates oficiais, ele continua funcionando. Duas diferenças do supabase-js: trocar a senha de uma conta que já tem senha pede current_password, e updateUser({ email }) ainda não existe.

Database (PostgREST)

Com o SDK, use db.from() exatamente como no supabase-js — o Accept-Profile do projeto vai sozinho:

tasks.ts — com o SDK (recomendado)
const { data } = await db.from('tasks').select('*').eq('done', false)
await db.from('tasks').insert({ title: 'Comprar pão' })
await db.from('tasks').update({ done: true }).eq('id', taskId)
await db.from('tasks').delete().eq('id', taskId)

// Insert que devolve a linha criada: encadeie .select()
const { data: criada } = await db.from('tasks').insert({ title: 'Pagar a luz' }).select().single()

// Upsert: cria ou atualiza pela chave única
await db.from('perfis').upsert({ id: userId, nome: 'Ana' }, { onConflict: 'id' })

// OU, negação, busca textual e contagem sem trazer as linhas
await db.from('tasks').select('*').or('done.eq.false,prioridade.gt.3')
await db.from('tasks').select('*').not('status', 'in', ['arquivada', 'cancelada'])
await db.from('artigos').select('id, titulo').textSearch('corpo', 'banco de dados', { type: 'websearch', config: 'portuguese' })
const { count } = await db.from('tasks').select('*', { count: 'exact', head: true }).eq('done', false)

Como no supabase-js v2, insert, update, upsert e delete só devolvem as linhas quando você encadeia .select(). Assim o insert funciona mesmo numa tabela em que a RLS deixa inserir mas não ler (um formulário de contato, por exemplo). Também existem match, filter, contains, containedBy, overlaps, range, abortSignal e throwOnError.

Sem o SDK, bata no REST cru. O host é https://api.superdb.com.br/<tabela> (path raiz, sem /rest/v1/); o tenant vai por header — Accept-Profile na leitura, Content-Profile na escrita:

tasks.ts — fetch cru (sem SDK)
const ANON_KEY = process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
const DATA_URL = 'https://api.superdb.com.br'

// Select (leitura → Accept-Profile)
const res = await fetch(`${DATA_URL}/tasks?done=eq.false`, {
  headers: { 'Authorization': `Bearer ${ANON_KEY}`, 'apikey': ANON_KEY,
             'Accept-Profile': 'proj_meuapp' }
})
const data = await res.json()

// Insert (escrita → Content-Profile)
await fetch(`${DATA_URL}/tasks`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${ANON_KEY}`, 'apikey': ANON_KEY,
             'Content-Type': 'application/json', 'Content-Profile': 'proj_meuapp',
             'Prefer': 'return=minimal' },
  body: JSON.stringify({ title: 'Comprar pão' })
})

Storage

Com o SDK, use db.storage.from('bucket') — o token de storage é mintado automaticamente. O nome do bucket é o completo, proj_<slug>_<nome>, o mesmo que aparece no painel: o bucket avatares do projeto meuapp se chama proj_meuapp_avatares. Com o nome curto, o Storage não acha o bucket.

upload.ts — com o SDK
const BUCKET = 'proj_meuapp_avatares'

// Upload (upsert substitui o arquivo que já existe)
await db.storage.from(BUCKET).upload('user1.png', file, { upsert: true })

// Download
const { data } = await db.storage.from(BUCKET).download('user1.png')

// URL pública (bucket público) e miniatura 200×200
const { data: { publicUrl } } = db.storage.from(BUCKET).getPublicUrl('user1.png')
const thumb = publicUrl.replace('/object/', '/render/image/') + '?width=200&height=200&resize=cover'

// URL assinada (bucket privado), válida por 1 hora
const { data: signed } = await db.storage
  .from('proj_meuapp_documentos').createSignedUrl('contrato.pdf', 3600)
⚠️

Não aponte @supabase/storage-js direto no storage.superdb.com.br — o storage-api valida HS256 e as chaves do data-plane são ES256 (retorna 401). É preciso trocar por um token de storage em POST https://auth.superdb.com.br/st/v1/token.

Trocando a chave por um token de storage

A credencial vai no cabeçalho Authorization: Bearer. O endpoint não lê nenhum campo do corpo e não usa o cabeçalho X-SuperDB-Project — o projeto sai da própria chave, no claim project_schema.

O token de storage herda o papel da chave que o pediu. É essa linha que decide se o upload funciona:

Chave no AuthorizationPapel do tokenO que ele faz
anon (pública)anonSomente leitura, e apenas de bucket público
service_role (só no servidor)authenticatedEnviar, apagar, criar bucket
data_plane_token da sessãoauthenticatedLer, enviar e apagar em todos os buckets do projeto — o Storage ainda não separa arquivos por usuário
⚠️

Bucket privado só aceita escrita de token com papel diferente de anon. Se o upload volta 403 new row violates row-level security policy, é quase sempre isto: a chave que gerou o token era a anon. Decodifique o token devolvido e confira o claim role antes de procurar em outro lugar.

A anon key é pública — ela vive no JavaScript da página. Por isso ela não escreve: até 31/08/2026 escrevia, e qualquer visitante de um site podia apagar ou ler arquivo de bucket privado.

upload-servidor.ts — bucket privado, sem expor a service_role
// 1) Troque a chave por um token de storage. A credencial vai no HEADER.
//    Nada no corpo é lido: mandar { "token": ... } não muda nada.
const mint = await fetch('https://auth.superdb.com.br/st/v1/token', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.SUPERDB_SERVICE_ROLE_KEY}` },
})
const { token, url, role, pode_escrever } = await mint.json()

// 2) Confira o papel ANTES de subir — é o que explica um 403 depois.
//    A resposta já diz: não precisa decodificar o JWT (e não use atob nele,
//    que é base64url e quebra o atob em parte dos tokens).
if (!pode_escrever) {
  throw new Error(`chave errada: token veio com role=${role}, não escreve em bucket privado`)
}

// 3) Suba no storage-api com o token mintado.
await fetch(`${url}/object/${bucket}/${caminho}`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': arquivo.type },
  body: arquivo,
})

Para agir como o usuário logado em vez de como o servidor, troque a chave do passo 1 pelo data_plane_token que veio no login — o mesmo campo que o Data Plane usa. O access_token da sessão não serve aqui: ele é PASETO, e este endpoint valida JWT ES256.

URL assinada: mostrar e baixar arquivo privado

Com o token de storage em mãos, o servidor pede uma URL temporária e entrega só ela ao navegador. A service_role nunca sai do backend, e o bucket continua privado.

url-assinada.ts
const r = await fetch(`${url}/object/sign/${bucket}/${caminho}`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ expiresIn: 300 }),   // SEGUNDOS. não é expires_in
})
const { signedURL } = await r.json()          // atenção: URL em maiúsculas

// signedURL é RELATIVO. Sem juntar com `url`, o navegador resolve contra o
// SEU domínio e devolve 404 — que é o erro mais comum aqui.
const paraOUsuario = `${url}${signedURL}`

// mesma URL serve para os dois. O `&` é porque ela já traz ?token=
const paraBaixar = `${paraOUsuario}&download=${encodeURIComponent('Relatorio.pdf')}`
⚠️

Erro no Storage privado quase nunca diz “sem permissão”. Diz “não encontrado”. A RLS torna o objeto invisível em vez de proibido, para não confirmar a existência de arquivo a quem não pode vê-lo:

assinar com token anon .......... {"error":"not_found","message":"Object not found"}
GET /object/public/ em privado .. {"error":"Bucket not found"}
GET direto sem Authorization .... {"error":"Bucket not found"}

Então Object not found num arquivo que você sabe que existe significa credencial errada, não caminho errado. Se a sua aplicação traduz erro de Storage para “sem permissão”, mostre o corpo original enquanto depura — a tradução manda procurar no lugar errado.

A URL vale só pelo expiresIn; depois disso responde 400 InvalidJWT. Por isso gere-a no clique, e não ao montar a lista: com prazo curto, uma URL criada no carregamento da tela pode já ter expirado quando o usuário clicar.

Nome do arquivo: acento é recusado

O caminho do objeto aceita espaço, parênteses, vírgula, & e maiúscula — mas não aceita caractere acentuado. Subir Relatório Final.pdf devolve:

HTTP 400 {"statusCode":"400","error":"InvalidKey","message":"Invalid key: Relatório Final.pdf"}

Em aplicação brasileira isso aparece cedo — Relatório, Licitação, Orçamento, Ata de Reunião. Guarde o nome original no seu banco para exibir, e mande ao Storage uma versão sem acento:

nome-seguro.ts
// NFD separa a letra do acento; a faixa \u0300-\u036f são os acentos.
export const nomeSeguro = (nome: string) =>
  nome.normalize('NFD').replace(/[\u0300-\u036f]/g, '')

nomeSeguro('Relatório Final (v2).pdf')  // 'Relatorio Final (v2).pdf'  ✅
💡

Limite por arquivo: é uma propriedade do bucket, e o padrão depende de como ele nasceu. Criado pelo painel (ou pela API do SuperDB), nasce com 50 MB. Criado direto no storage-api (POST /bucket, que é o caminho do storage-js), nasce sem limite nenhum. Se você cria buckets por esse caminho e quer o teto, passe file_size_limit em bytes na criação.

Cota do plano: no plano Grátis, ao passar de 1 GB de storage o /st/v1/token para de emitir token e responde 402. O upload deixa de funcionar por cota, não por erro de código.

Realtime

db.channel() ainda é stub. Para Realtime, minte um token em POST https://auth.superdb.com.br/rt/v1/token e conecte o @supabase/realtime-js com ele. A credencial que pede o token decide o que chega: com o data_plane_token da sessão, o realtime entrega as linhas que o usuário leria pela API; com a anon key, só o que um visitante sem login leria.

realtime.ts
import { RealtimeClient } from '@supabase/realtime-js'

// 1) Minta o token com a credencial de quem está usando o app
async function tokenDeRealtime() {
  const credencial = (await db.auth.getDataPlaneToken()) ?? ANON_KEY // sem login: a anon key
  const res = await fetch('https://auth.superdb.com.br/rt/v1/token', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${credencial}` }
  })
  return res.json() // { token, url, papel_de_origem, expires_in }
}

// 2) Conecta com o token mintado
const { token, url } = await tokenDeRealtime() // url = o socket do SEU projeto; use sempre este
const rt = new RealtimeClient(url, { params: { apikey: token } })
const channel = rt.channel('proj_meuapp:tasks-changes', { config: { private: true } })
  .on('postgres_changes',
    { event: 'INSERT', schema: 'proj_meuapp', table: 'tasks' },
    (payload) => console.log(payload.new)
  )
  .subscribe()

// 3) O token vale 1 hora. Troque antes de vencer, e ao entrar ou sair da conta:
//    rt.setAuth((await tokenDeRealtime()).token)
🔒

O realtime segue o RLS da tabela. Cada inscrito recebe só as linhas que a credencial dele leria pela API, e a tabela precisa estar com RLS ligada para entrar no realtime. Duas diferenças para a API: regra que consulta outra tabela precisa usar uma função security definer (o realtime confere a regra com um papel próprio, que não lê as outras tabelas; sem isso ela não vale lá, e o painel avisa ao ativar); e um DELETE chega a quem está inscrito na tabela só com a chave primária da linha apagada. A receita de chat mostra tudo isso junto.

Broadcast e presence: canal privado com o nome do projeto

Mensagens entre clientes (broadcast) e quem está online (presence) não passam por tabela nenhuma, então o RLS não as filtra. Use canal privado, com o nome começando pelo schema do projeto e dois-pontos: o servidor só deixa entrar e mandar quem tem token do mesmo projeto.

canal privado
const sala = rt.channel('proj_meuapp:sala-1', { config: { private: true } })
  .on('broadcast', { event: 'digitando' }, ({ payload }) => mostrar(payload))
  .on('presence', { event: 'sync' }, () => atualizarOnline(sala.presenceState()))
  .subscribe()

Canal público é identificado só pelo nome e é compartilhado entre todos os projetos da plataforma: um "sala-1" público de outro projeto é o mesmo canal. Por isso os exemplos acima usam canal privado também para postgres_changes. Ele sozinho não vaza — cada evento é roteado pela inscrição, com o RLS da tabela — mas o canal que hoje só escuta a tabela é o mesmo que amanhã ganha um "fulano está digitando". Uma regra só, sem pegadinha.

@superdb/auth-js

Só o auth, sem o cliente de dados: login por senha, código por e-mail ou pelo WhatsApp, login social e MFA TOTP. O @superdb/client já inclui tudo isto em .auth; use o @superdb/auth-js sozinho quando o app não fala com a API de dados.

auth-br.ts
import { createSuperDB } from '@superdb/auth-js'

const { auth } = createSuperDB({
  url: 'https://auth.superdb.com.br',
  anonKey: process.env.SUPERDB_ANON_KEY!,
  project: 'meu_projeto',
})

// Cadastro com dados extras: vão em options.data e viram user_metadata
await auth.signUp({
  email: 'user@exemplo.com.br',
  password: 'senha-segura',
  options: { data: { nome: 'Ana' } },
})

// Código por WhatsApp (com o canal configurado no projeto)
await auth.signInWithOtp({ phone: '+5511999998888', options: { channel: 'whatsapp' } })

// Confirmar o código: o mesmo telefone, no mesmo formato
await auth.verifyOtp({ phone: '+5511999998888', code: '123456' })

// MFA TOTP, com a sessão ativa: data traz secret e uri (otpauth://, para o QR code)
const { data } = await auth.mfa.enroll({ factorType: 'totp' })

Preview no Studio

Tudo que você faz pelo SDK aparece em tempo real no Studio. Veja seus dados, queries e logs em superdb.com.br/dash/<projeto>:

superdb.com.br/dash/meu-app/editor
v0.4.2
tasks.sql ×
1-- query gerada pelo SDK
2select * from proj_meuapp.tasks
3 where done = false;

TypeScript

Ambos os SDKs são totalmente tipados:

types.ts
import type { User, Session } from '@superdb/client'
import type { SuperDBUser, AuthError, SessionStorage } from '@superdb/auth-js'
💡

Dica: Use @superdb/client se você já tem código Supabase. Use @superdb/auth-js se quer aproveitar features 100% BR (CPF, WhatsApp, audit).

Essa página ajudou?