Pular para o conteúdo
🧭 CONCEITOS

Tokens e sessões: qual crachá abre qual porta.

Um login no SuperDB devolve três tokens, e cada um serve para um lugar. Esta página mostra qual é qual, como a sessão vive e se renova, o que o RLS lê de dentro do token e como conferir um token no seu próprio servidor.

Os tokens do SuperDB

TokenFormatoValidadeServe para
access_tokenPASETO v4 (começa com v4.public.), Ed25519, chave por projeto1 horaA API de auth (auth.superdb.com.br/auth/v1/*)
data_plane_tokenJWT ES256, chave da plataforma com JWKS público1 horaA API de dados (api.superdb.com.br)
refresh_tokenOpaco (aleatório)30 dias, troca a cada usoPedir uma sessão nova
Chave anonJWT ES256, role: anon, sem vencimentoAté você gerar outraIdentificar o projeto no app; pública
Chave service_roleJWT ES256, role: service_role, sem vencimentoAté você gerar outraSó no servidor: ignora o RLS
Token de storageJWT HS256 curto, emitido em /st/v1/token1 horaUpload e download de arquivos
Token de realtimeJWT HS256 curto, emitido em /rt/v1/token1 horaO WebSocket do realtime

Os três primeiros chegam juntos em todo login (signInWithPassword, verifyOtp, exchangeCodeForSession) e em todo refresh. O SDK guarda a sessão inteira e escolhe sozinho o token certo para cada chamada.

Qual token vai em cada lugar

Só importa para quem chama a API por HTTP direto, sem o SDK:

DestinoHeaders
Auth, como o usuário (/auth/v1/user, MFA, sair)Authorization: Bearer <access_token>, apikey e X-SuperDB-Project
Dados, como o usuárioAuthorization: Bearer <data_plane_token>
Dados, sem loginSó apikey: <chave anon>
Dados, no seu servidor, acima do RLSSó apikey: <service_role>
⚠️

Não ponha a chave anon no Authorization de um usuário logado. Quando os dois headers chegam, vale o Authorization: o usuário vira anônimo, e as consultas com RLS voltam vazias, sem erro. E o access_token na API de dados devolve 401, porque ela só aceita o data_plane_token.

Ciclo de vida da sessão

  1. O login devolve os três tokens. O SDK guarda a sessão no storage que você passou ao createClient; sem ele, no localStorage do navegador, ou em memória no Node.
  2. Depois de 1 hora, o access_token e o data_plane_token vencem. O SDK (desde a 0.3) renova sozinho um minuto antes de vencer, e o onAuthStateChange avisa com TOKEN_REFRESHED. Quem bate na API sem o SDK chama POST /auth/v1/token/refresh antes de vencer.
  3. Cada refresh devolve um refresh_token novo e aposenta o anterior. Reusar um refresh já trocado é tratado como roubo: a família inteira de sessões cai, e a pessoa precisa entrar de novo.
  4. db.auth.signOut() revoga o refresh da sessão atual; signOut('global'), o de todas as sessões daquela conta. O access_token e o data_plane_token já emitidos continuam válidos até vencer (no máximo 1 hora): token assinado não tem como ser recolhido.
💡

Com várias abas abertas, cada uma tentaria renovar com o mesmo refresh, e a segunda derrubaria a sessão. O SDK renova uma aba por vez (Web Locks), e a aba que esperou adota a sessão que a outra gravou. O sessao.ts dos templates oficiais faz o mesmo para quem ainda está na 0.2. No Next.js, a renovação fica no proxy.ts, com a sessão em cookies.

O que o RLS lê do token

Nas policies, as funções do schema auth leem o data_plane_token da requisição:

FunçãoDevolve
auth.uid()O id do usuário (claim sub), ou null sem login
auth.email()O e-mail, quando a conta tem
auth.role()authenticated, anon ou service_role
auth.jwt()Todos os claims, como jsonb: sub, email, role, project_id, project_schema, aud, exp

São só essas quatro: o schema auth do SuperDB não tem tabelas. Não existe auth.users; para ligar uma linha ao dono, use user_id uuid not null default auth.uid().

E claims próprios, como plano ou papel?

Não existe hook que altere o token. Guarde o que a policy precisa numa tabela e consulte na própria policy. De quebra, a regra vale na hora em que o dado muda, sem esperar o token ser renovado:

papel numa tabela, lido pela policy
create table if not exists papeis (
  user_id uuid primary key,
  papel text not null check (papel in ('membro', 'admin'))
);
alter table papeis enable row level security;
drop policy if exists papeis_ler_o_proprio on papeis;
create policy papeis_ler_o_proprio on papeis
  for select to authenticated using (user_id = auth.uid());
revoke all on papeis from anon;
grant select on papeis to authenticated;

-- em outra tabela: só admin lê
drop policy if exists relatorios_so_admin on relatorios;
create policy relatorios_so_admin on relatorios
  for select to authenticated
  using (exists (select 1 from papeis p where p.user_id = auth.uid() and p.papel = 'admin'));

Quem dá o papel é o seu servidor, com a service key, depois de conferir quem pediu. O usuário só lê o próprio.

Conferir o token no seu servidor

Para saber quem chamou a sua API, confira a assinatura do data_plane_token com o JWKS público do SuperDB. Nunca autorize lendo o payload sem conferir: qualquer um monta um payload.

servidor — com a biblioteca jose
import { createRemoteJWKSet, jwtVerify } from 'jose'

const JWKS = createRemoteJWKSet(new URL('https://auth.superdb.com.br/.well-known/jwks.json'))

export async function usuarioDaRequisicao(req: Request) {
  const token = req.headers.get('authorization')?.replace(/^Bearer\s+/i, '')
  if (!token) return null
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://auth.superdb.com.br',
      audience: 'https://api.superdb.com.br',
    })
    // o token precisa ser do SEU projeto e de um usuário logado
    if (payload.project_schema !== `proj_${process.env.SUPERDB_PROJECT}`) return null
    if (payload.role !== 'authenticated') return null
    return { id: payload.sub as string, email: payload.email as string | undefined }
  } catch {
    return null // assinatura inválida, vencido, ou de outro emissor
  }
}

No navegador, mande o data_plane_token da sessão: (await db.auth.getSession()).data.session?.data_plane_token. Se preferir não lidar com JWKS, chame GET https://auth.superdb.com.br/auth/v1/user com o access_token: custa uma ida à rede por requisição, mas responde também se a conta foi apagada.

Ler o token no navegador

Para mostrar algo na tela, como o horário em que a sessão vence, dá para ler o payload do data_plane_token, que é um JWT comum. O access_token é PASETO, com outro formato: não tente abrir com split('.').

navegador — só para exibir, nunca para autorizar
function lerClaims(jwt: string) {
  const base64 = jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')
  const bytes = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0))
  return JSON.parse(new TextDecoder().decode(bytes)) as { sub: string; email?: string; exp: number }
}

const { data: { session } } = await db.auth.getSession()
if (session?.data_plane_token) {
  const { exp } = lerClaims(session.data_plane_token)
  console.log('a sessão vence às', new Date(exp * 1000).toLocaleTimeString('pt-BR'))
}

Armadilhas comuns

  • Autorizar lendo o payload sem conferir a assinatura. No servidor, use o JWKS, como acima.
  • Usar o SDK 0.2 e esperar que ele renove. Até a 0.2 ele não renovava: sem refreshSession(), a sessão caía em 1 hora e as consultas passavam a voltar 401. Atualize para a 0.3.
  • Duas abas renovando com o mesmo refresh. A segunda derruba a família de sessões. Renove uma vez por vez.
  • Sessão no localStorage e XSS. Um script injetado na página lê o localStorage. Se o seu app renderiza HTML de terceiros, prefira a sessão em cookie httpOnly, como no template Next.js.
  • A service key no navegador. Ela ignora o RLS de todas as tabelas. Só no servidor, em variável de ambiente.

Termos relacionados

Essa página ajudou?