Quando alguém estima um SaaS novo, o login costuma ser o primeiro item da lista e o mais barato. Duas caixas de texto e um botão. Já vi essa estimativa ser feita em voz alta várias vezes, inclusive por mim.

O problema não é a dificuldade. É o comprimento da lista.

A lista que ninguém escreve na estimativa

Cadastro e login são as duas telas que você imaginou. Depois delas vem o e-mail de confirmação, e com ele o caso do usuário que não recebeu, que precisa de um botão de reenvio, que só faz sentido se você souber distinguir "senha errada" de "conta ainda não confirmada". São mensagens de erro diferentes e caminhos diferentes.

Recuperação de senha é outro fluxo inteiro: a tela que pede o e-mail, o link com token, a página que lê esse token da URL, a validação, a tela de nova senha e o que acontece quando o link já expirou. No CastorStack o token de recuperação chega no fragmento da URL e é lido antes de qualquer coisa renderizar, porque um fragmento sobrevive mal a redirecionamento.

Aí tem a sessão. Onde ela fica, quanto dura, o que acontece quando o access token expira com o usuário no meio de um formulário preenchido. Refresh silencioso ou logout na cara dele são decisões de produto disfarçadas de decisão técnica.

Nada disso é difícil. Some tudo e você tem duas semanas, não uma tarde.

A metade que não tem tela

O frontend só entrega um token. Alguém precisa acreditar nele.

A API do CastorStack valida JWT do Supabase buscando as chaves públicas no JWKS do próprio projeto, com rotação de chave tratada e tolerância de um minuto de diferença de relógio entre as máquinas. Emissor, audiência e validade são conferidos. Existe um caminho HS256 para desenvolvimento local, que é o tipo de atalho que precisa estar isolado e documentado, porque é exatamente o tipo de coisa que vaza para produção quando ninguém olha.

Essa parte não aparece em nenhuma captura de tela do produto e é a única que impede alguém de forjar um token e entrar como outra pessoa.

Saber quem é não é saber o que pode

Autenticação responde "quem é você". Sobra a pergunta que o produto realmente faz: você pode ver esta página, chamar este endpoint, baixar este arquivo.

São eixos separados. O usuário está autenticado, mas a assinatura venceu ontem. Está autenticado e pagando, mas o plano dele não inclui o recurso. Está autenticado, pagando e no plano certo, mas você desligou o recurso para todo mundo por causa de um incidente.

No CastorStack isso é resolvido com atributos nos endpoints e uma avaliação de acesso que consulta plano, recursos do plano e um kill switch por feature. O painel administrativo é uma aplicação separada, com deploy separado, e todas as rotas dele vivem sob um prefixo próprio protegido por papel de administrador. Separação física custa pouco e elimina uma classe inteira de erro.

O buraco que o Supabase abre por padrão

Este merece parágrafo próprio porque quase passou batido aqui.

O Supabase publica cada tabela do banco através de uma API REST automática, acessível com a chave anônima. Essa chave está no bundle do frontend, onde qualquer pessoa pode lê-la. É por design: a proteção real é o Row Level Security do Postgres, e ele não vem ligado sozinho nas tabelas que a sua API cria por migration.

A correção foi uma migration que liga RLS nas catorze tabelas gerenciadas pelo servidor, revoga as permissões dos papéis públicos e muda o privilégio padrão do schema, para que qualquer tabela criada dali em diante já nasça fechada. A última parte é a que importa: a proteção precisa ser o padrão, não um item de checklist que alguém vai esquecer na próxima migration.

Erro em três idiomas

Se o produto vende fora do seu país, a mensagem "senha inválida" também precisa existir fora do seu idioma. As respostas de erro da API seguem RFC 7807 e são traduzidas conforme o cabeçalho de idioma da requisição, nos mesmos três idiomas do resto do sistema.

Detalhe pequeno, mas é o tipo de coisa que ninguém volta para arrumar depois. Fica em inglês para sempre.

O que não está aqui

Não tem autenticação de dois fatores. Não tem bloqueio de conta após tentativas erradas, nem política de senha escrita neste repositório: essas configurações vivem no painel do Supabase, e chamá-las de recurso do template seria mentira. Login social não está implementado. Não existe listagem e revogação de sessões ativas. Não há organizações nem times, o que foi decisão, não esquecimento: a unidade de escopo é o perfil, e multi-tenancy é o tipo de coisa que se adiciona quando um cliente pede, não antes.

O log de auditoria registra o que acontece na API, então mudanças de catálogo, provedor de pagamento e conteúdo ficam gravadas com autor, IP e o que mudou. Login e logout não aparecem lá, porque acontecem no Supabase e nunca tocam a API.

Prefiro essa lista escrita do que descoberta na semana do lançamento.

Por que isso importa mais agora

Escrever o CRUD ficou barato. Um modelo gera a tela de login em segundos, e ela até funciona.

O que ele não gera é a decisão de onde guardar o refresh token, o RLS ligado antes de alguém apontar um cliente REST para o seu banco, ou a lembrança de que reenviar confirmação é um fluxo e não um botão. Essas coisas você descobre uma vez, e é mais barato descobrir no código de outra pessoa.