Pular para o conteúdo
🧭 CONCEITOS

Service-role vs anon key — duas chaves, dois mundos.

O SuperDB dá duas API keys por projeto: a anon (pública, respeita RLS) e a service-role (privada, bypassa RLS). Usar a errada é a fonte número 1 de vazamentos. Aqui está a regra simples e exemplos.

O que é?

Toda projeto SuperDB tem 2 API keys (ambas são JWTs assinados, ambas vão no header Authorization):

anonservice-role
Claim roleanonservice_role
Identifica usuário?Não — só o projetoNão — só "sou admin"
Respeita RLS?SimNÃO — bypassa
Pode expor no client?Sim (env pública)JAMAIS
PermissãoO que RLS deixarTudo — qualquer schema, qualquer row
Onde ficaNEXT_PUBLIC_SUPERDB_ANON_KEYSUPERDB_SERVICE_ROLE_KEY (server only)

A regra é: anon vai no browser, service-role nunca sai do servidor.

Por que importa?

Vazar service-role é a falha de segurança mais comum em apps backend-as-a-service. Cenários reais:

  • Dev coloca SUPERDB_SERVICE_ROLE_KEY num NEXT_PUBLIC_* achando que é igual à anon — Next bundle expõe no JS do browser → todo mundo no mundo pode ler/escrever qualquer tabela.
  • Service-role commitado num .env que foi pro Git público.
  • Service-role num app mobile compilado — quem decompila tem o app inteiro.

Anon, por outro lado, é desenhada pra ser pública. Ela diz "sou um cliente do projeto X" — e nada mais. Sem RLS por trás, ela não permite ler/escrever nada útil.

🚨

Se você vazou a service-role: vá AGORA no Dashboard → Settings → API → "Rotate service-role key". Todos os tokens antigos são invalidados imediatamente. Atualize .env dos seus servidores.

Como funciona no SuperDB

As duas keys são JWTs HS256 assinados com o mesmo JWT secret do projeto. O que muda é o role no payload:

payload — anon
{
  "iss": "superdb",
  "ref": "acme-prod",
  "role": "anon",
  "iat": 1714000000,
  "exp": 2030000000
}
payload — service_role
{
  "iss": "superdb",
  "ref": "acme-prod",
  "role": "service_role",
  "iat": 1714000000,
  "exp": 2030000000
}

No Postgres, as duas roles existem como usuários reais:

postgres roles
CREATE ROLE anon NOINHERIT;
CREATE ROLE authenticated NOINHERIT;
CREATE ROLE service_role NOINHERIT BYPASSRLS;  -- ← essa

O atributo BYPASSRLS é o que faz a service-role ignorar todas as policies. Quando PostgREST faz SET ROLE service_role, RLS para de funcionar pra aquela transação.

Quando usar (e quando não)

Use anon em…

  • Código do client — browser, mobile, app desktop
  • Server Components que rodam por usuário — Next.js middleware que valida o JWT do user e faz queries em nome dele
  • Edge functions com identidade do user — Cloudflare Workers que recebem o JWT e fazem proxy

Use service-role em…

  • Admin scripts — seed data, migrations programáticas
  • Cron jobs — limpeza, agregações, relatórios noturnos
  • Webhooks de terceiros — Stripe/billing chega sem JWT de user; você precisa criar registros em nome do sistema
  • Background workers — fila que processa eventos sem usuário associado
  • Importação em batch — copiar 10M rows de um CSV legado

NUNCA use…

  • Service-role no client — vaza o projeto inteiro
  • Anon no server pra fazer admin — anon respeita RLS; sem JWT de user, retorna vazio na maior parte das queries (o que parece "não funcionar")

Exemplos práticos

1. Client com anon (padrão)

lib/superdb-client.ts
import { createClient } from '@superdb/supabase-compat'

export const db = createClient(
  process.env.NEXT_PUBLIC_SUPERDB_URL!,
  process.env.NEXT_PUBLIC_SUPERDB_ANON_KEY!
)

// Quando o user loga, o SDK guarda o JWT dele em cookie
// e envia tanto a anon key (no header `apikey`) quanto o JWT
// do user (no header `Authorization`).
// RLS então roda com auth.uid() = user real.

2. Server Action com service-role pra admin op

app/admin/actions.ts — server only
'use server'

import { createClient } from '@superdb/supabase-compat'

// SUPERDB_SERVICE_ROLE_KEY: sem NEXT_PUBLIC_ ⇒ só server
const admin = createClient(
  process.env.SUPERDB_URL!,
  process.env.SUPERDB_SERVICE_ROLE_KEY!,
  { auth: { persistSession: false } }
)

export async function banUser(userId: string) {
  // Antes: cheque autorização EXPLICITAMENTE (você bypassou RLS!)
  const session = await getServerSession()
  if (session.user.role !== 'admin') throw new Error('forbidden')

  await admin.from('users').update({ banned: true }).eq('id', userId)
}
⚠️

Sempre que você usar service-role: ANTES da query, valide manualmente que o caller tem permissão. RLS não vai te salvar — você desligou ela.

3. MCP server usa service-role (intencional, roda local)

~/.cursor/mcp.json
{
  "mcpServers": {
    "superdb": {
      "command": "npx",
      "args": ["@superdb/mcp"],
      "env": {
        "SUPERDB_URL": "https://api.superdb.com.br",
        "SUPERDB_SERVICE_ROLE_KEY": "eyJhbGc..."
      }
    }
  }
}

Tudo bem porque o MCP server roda no seu computador, não no client de ninguém. Mesma analogia de psql com senha do banco.

Armadilhas comuns

  • Commitar service-role no Git. Mesmo num repo privado, rotacione já. GitHub scanners + leaks acontecem.
  • Prefixar service-role com NEXT_PUBLIC_. Next.js inclui qualquer NEXT_PUBLIC_* no bundle JS. Erro fatal — rotacione.
  • Usar service-role no client achando que "RLS protege". Não protege — service-role bypassa RLS por design.
  • Usar anon no server pra coisa admin. "Por que minha query retorna vazio?" — porque anon respeita RLS e o server não tem JWT de usuário válido. Use service-role.
  • Não rotacionar quando dev sai do time. Saiu da empresa com acesso? Rotacione service-role + JWT secret. Anon pode ficar (era pública mesmo).
  • Confundir apikey header com Authorization. apikey é a API key (anon ou service). Authorization é o JWT do user. SDK envia os dois.
  • Service-role em logs. Acidente comum em console.log(env). Adicione filtro no logger.
💡

Best practice: tenha uma env var SUPERDB_SERVICE_ROLE_KEY SÓ no servidor (Vercel server-side env, Coolify secrets, AWS Secrets Manager). Nunca prefixe com NEXT_PUBLIC_, VITE_, REACT_APP_.

Termos relacionados

Essa página ajudou?