Os tokens do SuperDB
| Token | Formato | Validade | Serve para |
|---|---|---|---|
access_token | PASETO v4 (começa com v4.public.), Ed25519, chave por projeto | 1 hora | A API de auth (auth.superdb.com.br/auth/v1/*) |
data_plane_token | JWT ES256, chave da plataforma com JWKS público | 1 hora | A API de dados (api.superdb.com.br) |
refresh_token | Opaco (aleatório) | 30 dias, troca a cada uso | Pedir uma sessão nova |
| Chave anon | JWT ES256, role: anon, sem vencimento | Até você gerar outra | Identificar o projeto no app; pública |
| Chave service_role | JWT ES256, role: service_role, sem vencimento | Até você gerar outra | Só no servidor: ignora o RLS |
| Token de storage | JWT HS256 curto, emitido em /st/v1/token | 1 hora | Upload e download de arquivos |
| Token de realtime | JWT HS256 curto, emitido em /rt/v1/token | 1 hora | O 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:
| Destino | Headers |
|---|---|
Auth, como o usuário (/auth/v1/user, MFA, sair) | Authorization: Bearer <access_token>, apikey e X-SuperDB-Project |
| Dados, como o usuário | Authorization: Bearer <data_plane_token> |
| Dados, sem login | Só apikey: <chave anon> |
| Dados, no seu servidor, acima do RLS | Só 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
- O login devolve os três tokens. O SDK guarda a sessão no
storageque você passou aocreateClient; sem ele, nolocalStoragedo navegador, ou em memória no Node. - Depois de 1 hora, o
access_tokene odata_plane_tokenvencem. O SDK (desde a 0.3) renova sozinho um minuto antes de vencer, e oonAuthStateChangeavisa comTOKEN_REFRESHED. Quem bate na API sem o SDK chamaPOST /auth/v1/token/refreshantes de vencer. - Cada refresh devolve um
refresh_tokennovo 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. db.auth.signOut()revoga o refresh da sessão atual;signOut('global'), o de todas as sessões daquela conta. Oaccess_tokene odata_plane_tokenjá 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ção | Devolve |
|---|---|
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:
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.
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('.').
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 voltar401. 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
localStoragee XSS. Um script injetado na página lê olocalStorage. Se o seu app renderiza HTML de terceiros, prefira a sessão em cookiehttpOnly, 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
- Service-role vs anon: as duas chaves do projeto.
- RLS explicado: as policies que usam
auth.uid(). - API REST → Autenticação: o
data_plane_tokenna prática. - SDK JS → Auth: login, sessão e renovação.
- RFC 7519 — JSON Web Token