<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Gabriel</title>
    <description>The latest articles on DEV Community by Gabriel (@bielcoelho).</description>
    <link>https://dev.to/bielcoelho</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F992390%2Fd1799ba3-98ec-4e79-8f78-74329fb8c1b7.png</url>
      <title>DEV Community: Gabriel</title>
      <link>https://dev.to/bielcoelho</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bielcoelho"/>
    <language>en</language>
    <item>
      <title>Como o brtech implementa anonimato de verdade: a arquitetura de privacidade por tras dos relatos</title>
      <dc:creator>Gabriel</dc:creator>
      <pubDate>Tue, 14 Jul 2026 17:17:25 +0000</pubDate>
      <link>https://dev.to/bielcoelho/como-o-brtech-implementa-anonimato-de-verdade-a-arquitetura-de-privacidade-por-tras-dos-relatos-1dli</link>
      <guid>https://dev.to/bielcoelho/como-o-brtech-implementa-anonimato-de-verdade-a-arquitetura-de-privacidade-por-tras-dos-relatos-1dli</guid>
      <description>&lt;h2&gt;
  
  
  Se eu sofrer um ataque e dumparem o banco de dados do brtech, o que o invasor encontra?
&lt;/h2&gt;

&lt;p&gt;Hashes irreversiveis. Nomes aleatorios. Tokens nulos. Zero emails. Zero IPs. Zero user agents.&lt;/p&gt;

&lt;p&gt;Nao e "confia que a gente nao vai vazar". E "mesmo que vaze, nao tem o que entregar".&lt;/p&gt;

&lt;p&gt;Esse post conta como chegamos nessa arquitetura - e os 6 dias de commits que transformaram uma solucao ingenua em anonimato de verdade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dia 1: "So guardar o hash, ne?"
&lt;/h2&gt;

&lt;p&gt;27 de junho. Primeiro commit do &lt;code&gt;auth.ts&lt;/code&gt;. O plano parecia simples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Solucao ingenua - Dia 1&lt;/span&gt;
&lt;span class="nx"&gt;databaseHooks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUnique&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emailHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;emailHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// "pronto, nao guardei email"&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Funcionava?&lt;/strong&gt; Tecnicamente sim. O email chegava no hook, era hasheado, e o campo &lt;code&gt;email&lt;/code&gt; ia como &lt;code&gt;null&lt;/code&gt; para o banco.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mas tinha um problema.&lt;/strong&gt; O email ainda transitava pelo hook do Better Auth. A lib recebia o payload do Google/GitHub/LinkedIn com o email em texto puro, chamava o &lt;code&gt;before&lt;/code&gt;, e so depois descartava. Se alguem logasse o &lt;code&gt;user&lt;/code&gt; dentro do hook - ou se o Better Auth fizesse algum log interno - o email estava la.&lt;/p&gt;

&lt;p&gt;Era melhor que guardar no banco. Mas nao era anonimato de verdade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dia 2: "anonymousId publico e ma ideia"
&lt;/h2&gt;

&lt;p&gt;28 de junho. O schema tinha um campo &lt;code&gt;anonymousId&lt;/code&gt; - 4 caracteres alfanumericos que identificavam o usuario publicamente nos perfis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model User {
  anonymousId String? @unique
  // ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A ideia era: em vez de mostrar o CUID interno (que e imutavel e poderia ser usado para correlacionar dados entre tabelas), mostramos um ID publico curto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;29 de junho. Removido.&lt;/strong&gt; O commit &lt;code&gt;ab781c3&lt;/code&gt; deletou o campo do schema, do auth, dos tipos, de tudo.&lt;/p&gt;

&lt;p&gt;O problema: qualquer ID estavel e visivel publicamente permite correlacionar contribuicoes ao longo do tempo. Se o usuario &lt;code&gt;aB3x&lt;/code&gt; posta um salario na Stone e amanha posta uma review no Nubank, da para juntar as pecas - especialmente se o cargo e senioridade forem parecidos.&lt;/p&gt;

&lt;p&gt;Nao ter ID publico nenhum tambem nao funcionava - o usuario precisa ver seu proprio historico. A solucao final foi o &lt;code&gt;anonymousId&lt;/code&gt; virar um derivado do &lt;code&gt;emailHash&lt;/code&gt; (visivel so para o proprio usuario logado), mas deixamos isso para depois.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dia 3: "Usuario Anonimo com foto de dado"
&lt;/h2&gt;

&lt;p&gt;2 de julho. Commit &lt;code&gt;b2e1f13&lt;/code&gt;. Reescrevemos o &lt;code&gt;before&lt;/code&gt; hook para nunca mais deixar o email original tocar campos que vao para o banco:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateRandomEmail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;@anonimizado.brtech.fyi`&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`Anonimo #&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generateImage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`https://api.dicebear.com/6.x/initials/svg?seed=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;emailHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hashEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;// unica coisa que persiste&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateRandomEmail&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;       &lt;span class="c1"&gt;// falso&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateName&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;               &lt;span class="c1"&gt;// "Anonimo #aB3xK2m"&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateImage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;             &lt;span class="c1"&gt;// avatar DiceBear aleatorio&lt;/span&gt;
        &lt;span class="na"&gt;emailVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;seniorityLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;currentRole&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora o fluxo era:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Google manda &lt;code&gt;{ email: "joao@gmail.com", name: "Joao Silva", image: "https://..." }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;O hook gera &lt;code&gt;emailHash&lt;/code&gt; via HMAC-SHA256&lt;/li&gt;
&lt;li&gt;Substitui &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt; por dados falsos&lt;/li&gt;
&lt;li&gt;O banco &lt;strong&gt;nunca ve&lt;/strong&gt; o email real&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Mas ainda faltava uma camada.&lt;/strong&gt; Os tokens OAuth (access token, refresh token) e os dados de sessao (IP, user agent) continuavam sendo persistidos pelo Better Auth automaticamente. Esses tokens sao a chave para acessar a conta Google/GitHub do usuario - se vazarem, o invasor pode se passar por ele.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dia 4: O lockdown (o commit que mudou tudo)
&lt;/h2&gt;

&lt;p&gt;5 de julho. Commit &lt;code&gt;32ebbbc&lt;/code&gt;. O "privacy lockdown".&lt;/p&gt;

&lt;p&gt;Duas descobertas aconteceram nesse dia:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. O Better Auth persiste tokens OAuth por padrao.&lt;/strong&gt; Cada login guarda &lt;code&gt;accessToken&lt;/code&gt;, &lt;code&gt;refreshToken&lt;/code&gt;, &lt;code&gt;idToken&lt;/code&gt;, &lt;code&gt;scope&lt;/code&gt; na tabela &lt;code&gt;Account&lt;/code&gt;. Esses tokens dao acesso a API do Google/GitHub/LinkedIn em nome do usuario.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. O Better Auth atualiza os tokens em TODO login.&lt;/strong&gt; O campo &lt;code&gt;account.updateAccountOnSignIn: true&lt;/code&gt; (padrao) reescreve os dados da conta a cada sessao.&lt;/p&gt;

&lt;p&gt;A solucao foi atacar em todas as frentes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ipAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;disableIpTracking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// nunca armazena IP&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Nao atualiza tokens a cada login&lt;/span&gt;
  &lt;span class="nl"&gt;updateAccountOnSignIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

  &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;idToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessTokenExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;refreshTokenExpiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Segunda camada: zera email e imagem do User&lt;/span&gt;
      &lt;span class="c1"&gt;// apos o Better Auth terminar de criar a conta&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;emailVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;accessToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... mesmo tratamento&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;create&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ipAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// nunca armazena IP&lt;/span&gt;
        &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// nunca armazena user agent&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ipAddress&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;userAgent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;

&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;update&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;generateRandomEmail&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;   &lt;span class="c1"&gt;// re-randomiza a cada update&lt;/span&gt;
        &lt;span class="na"&gt;emailVerified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O &lt;code&gt;after&lt;/code&gt; do account e a jogada mais sutil aqui. Mesmo depois do &lt;code&gt;before&lt;/code&gt; limpar os campos, o Better Auth pode popular novamente o &lt;code&gt;User.email&lt;/code&gt; e &lt;code&gt;User.image&lt;/code&gt; durante a criacao da conta. O &lt;code&gt;after&lt;/code&gt; garante que, depois que a lib termina seu trabalho, a gente limpa de novo.&lt;/p&gt;

&lt;p&gt;Nesse mesmo dia criei o &lt;code&gt;PRIVACY_DATA_INVENTORY.md&lt;/code&gt; - um documento mapeando campo por campo do banco, o que contem, e por que pode ou nao ser anonimizado. Virou a referencia canonica para qualquer decisao futura sobre privacidade.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dia 5: Cross-provider linking sem email
&lt;/h2&gt;

&lt;p&gt;6 de julho. Commits &lt;code&gt;5183060&lt;/code&gt; → &lt;code&gt;673df35&lt;/code&gt; → &lt;code&gt;c7c2b3d&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Agora o banco nao tinha email. Beleza. Mas e o cenario onde o usuario faz login com Google num dia e com GitHub no outro?&lt;/p&gt;

&lt;p&gt;Sem email no banco, o Better Auth nao consegue correlacionar as contas. Ele faz &lt;code&gt;findOne(User, { email: "joao@gmail.com" })&lt;/code&gt; e nao acha nada - porque o &lt;code&gt;email&lt;/code&gt; no banco e &lt;code&gt;aB3xK2m9nQpL4rSt@anonimizado.brtech.fyi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A solucao: interceptar as queries do adapter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;O Better Auth usa um adapter de banco de dados para todas as operacoes. Se eu conseguisse interceptar as chamadas &lt;code&gt;findOne&lt;/code&gt; no model &lt;code&gt;User&lt;/code&gt; filtrando por &lt;code&gt;email&lt;/code&gt;, poderia redirecionar para &lt;code&gt;emailHash&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withEmailHashLookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createAdapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createAdapter&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Intercepta queries de User por email&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Tenta achar por emailHash primeiro&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emailHash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hashEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
          &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashResult&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hashResult&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="c1"&gt;// Fallback: busca normal por email&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;where&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// O wrapper tambem precisa funcionar dentro de transacoes&lt;/span&gt;
    &lt;span class="na"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E na configuracao do auth:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;accountLinking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;trustedProviders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;google&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;github&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;linkedin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;microsoft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;allowDifferentEmails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;updateUserInfoOnLink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora o fluxo e:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Usuario faz login com Google → &lt;code&gt;emailHash&lt;/code&gt; gerado, conta criada&lt;/li&gt;
&lt;li&gt;Usuario faz login com GitHub (mesmo email) → o adapter intercepta a query por &lt;code&gt;email&lt;/code&gt;, traduz para &lt;code&gt;emailHash&lt;/code&gt;, acha o mesmo usuario&lt;/li&gt;
&lt;li&gt;A conta GitHub e linkada ao mesmo usuario sem que o email jamais toque o banco&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Bug sutil que quase passou:&lt;/strong&gt; o adapter usava &lt;code&gt;model === 'User'&lt;/code&gt; (case-sensitive) mas o Better Auth internamente chama com &lt;code&gt;'user'&lt;/code&gt; (lowercase) em alguns metodos. Corrigi no commit &lt;code&gt;c7c2b3d&lt;/code&gt; mudando para &lt;code&gt;model.toLowerCase() === 'user'&lt;/code&gt;. Isso custou uma hora de debug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; pensando agora, nem precisava desse adapter todo. Era so gerar o hash e fazer append de &lt;code&gt;@anonymized.brtech.fyi&lt;/code&gt; como email fake - o Better Auth faria o lookup por email normalmente e encontraria o usuario pelo hash deterministico. Sem adapter. Sem wrapper. Sem interceptar query. Mas tambem nao tenho certeza se funcionaria com account linking cross-provider... e ja que ta funcionando assim, vai ficar assim.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dia 6: Soft delete que apaga ate o hash
&lt;/h2&gt;

&lt;p&gt;Uma vez por mes alguem pede para deletar a conta. A LGPD exige isso.&lt;/p&gt;

&lt;p&gt;Mas se eu so marco &lt;code&gt;status: 'deleted'&lt;/code&gt;, o hash continua la. E hashes podem ser atacados por forca bruta se o espaco de busca for pequeno (ex: "quero saber se &lt;a href="mailto:joao@gmail.com"&gt;joao@gmail.com&lt;/a&gt; tem conta no brtech" → gero o hash e comparo).&lt;/p&gt;

&lt;p&gt;A solucao: sobrescrever o hash com lixo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deleted&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;deletedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;emailHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`deleted-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;nanoid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// hash original e destruido&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As contribuicoes (salarios, reviews, entrevistas) &lt;strong&gt;nao sao deletadas&lt;/strong&gt; - elas permanecem anonimas, alimentando as medias e rankings. Mas o ponteiro entre elas e o usuario some para sempre.&lt;/p&gt;

&lt;p&gt;O que acontece se o usuario criar uma conta nova depois? Vai gerar um &lt;code&gt;emailHash&lt;/code&gt; novo, conta nova, historico zerado. A conta antiga e um fantasma - contribuicoes orfas que nao apontam para ninguem.&lt;/p&gt;




&lt;h2&gt;
  
  
  O que essa arquitetura NAO protege
&lt;/h2&gt;

&lt;p&gt;Para ser honesto, tem limites:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fingerprinting comportamental.&lt;/strong&gt; Se voce postar "Sou Senior na Stone, entrei em 2023, ganho R$18k CLT" e amanha tuitar exatamente a mesma frase, a correlacao e externa ao brtech. Nao tem como impedir que o usuario se auto-identifique fora da plataforma.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadados de moderacao.&lt;/strong&gt; Moderadores veem o conteudo das contribuicoes (precisam revisar). Nao veem identidade - mas veem que o usuario &lt;code&gt;Anonimo #aB3xK2m&lt;/code&gt; postou review X e salario Y. Se o usuario usar o mesmo cargo e empresa em ambas, e um padrao visivel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotacao do BETTER_AUTH_SECRET.&lt;/strong&gt; Se eu trocar a chave do HMAC, todos os hashes mudam. Ninguem consegue fazer login na conta antiga. Isso e documentado: rotacionar o secret requer uma migracao planejada que recalcula todos os &lt;code&gt;emailHash&lt;/code&gt; do banco. Nao e operacao de emergencia.&lt;/p&gt;




&lt;h2&gt;
  
  
  A funcao que amarra tudo
&lt;/h2&gt;

&lt;p&gt;No final, 14 linhas de codigo seguram o castelo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/hash-email.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;hashEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BETTER_AUTH_SECRET&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BETTER_AUTH_SECRET is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deterministico. Irreversivel. Sem sal (o secret e o sal). A mesma funcao e usada em 4 lugares: auth hook, adapter, seed script, e representative lookup. Se algum lugar usar um algoritmo diferente, usuarios duplicados aparecem.&lt;/p&gt;

&lt;p&gt;E essa funcao que me deixa dormir tranquilo. Se um dia alguem pedir acesso ao banco com um mandado judicial, o que vou entregar sao hashes HMAC-SHA256, nomes &lt;code&gt;Anonimo #&lt;/code&gt; aleatorios, e tokens nulos.&lt;/p&gt;

&lt;p&gt;E salarios que nao apontam para ninguem.&lt;/p&gt;




&lt;p&gt;O codigo esta no ar. A versao de preview em &lt;a href="https://dev.brtech.fyi" rel="noopener noreferrer"&gt;dev.brtech.fyi&lt;/a&gt; tem dados ficticios para testar. A versao oficial em &lt;a href="https://brtech.fyi" rel="noopener noreferrer"&gt;brtech.fyi&lt;/a&gt; já tem algumas contribuições, com dados reais.&lt;/p&gt;

&lt;p&gt;O repo e aberto: github.com/tuf/brtech (ou onde estiver hospedado).&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>privacy</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Consegui negociar R$50k a mais por ano porque um desconhecido compartilhou como foi a entrevista dele. Entao construi o brtech.</title>
      <dc:creator>Gabriel</dc:creator>
      <pubDate>Tue, 14 Jul 2026 12:06:49 +0000</pubDate>
      <link>https://dev.to/bielcoelho/consegui-negociar-r50k-a-mais-por-ano-porque-um-desconhecido-compartilhou-como-foi-a-entrevista-15eg</link>
      <guid>https://dev.to/bielcoelho/consegui-negociar-r50k-a-mais-por-ano-porque-um-desconhecido-compartilhou-como-foi-a-entrevista-15eg</guid>
      <description>&lt;p&gt;Antes da entrevista, encontrei um relato no Teamlyzer - uma plataforma portuguesa que mistura Reclame Aqui com Levels.fyi. La tinha um relato dizendo que a empresa costumava focar bastante em &lt;strong&gt;filas (queues)&lt;/strong&gt; para a entrevista tecnica de backend.&lt;/p&gt;

&lt;p&gt;Estudei exatamente aquilo. Cheguei muito mais preparado.&lt;/p&gt;

&lt;p&gt;Nessa mesma plataforma tambem da para ver salarios. Vendo o que outras pessoas tinham compartilhado, eu que ia pedir €33k acabei pedindo €45k, eles iam pagar inicialmente €30k.&lt;br&gt;
Fechamos em &lt;strong&gt;€41 mil por ano&lt;/strong&gt; (cerca de R$50k a mais do que eu ganhava antes). Pode nao parecer muito, mas e bastante em Portugal.&lt;/p&gt;

&lt;p&gt;Enfim, voltei para o Brasil e comecei a procurar emprego de novo e &lt;strong&gt;Senti falta de algo parecido.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No maximo voce consegue usar a busca em grupos do facebook como a APDA ou achar um relato de 3 anos atras no Reddit que esta &lt;code&gt;[removido pelo usuario]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Decidi desenvolver e lancar a &lt;strong&gt;brtech&lt;/strong&gt; para cobrir esses pontos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tres objetivos
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Centralizar salarios&lt;/strong&gt; como o Levels.fyi, mas focado no mercado BR. Salario base, bonus, VR/VA, modelo de trabalho (CLT/PJ/Cooperado), senioridade. Tudo em real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Relatos de entrevista.&lt;/strong&gt; Para o candidato saber o que esperar antes de entrar num teste tecnico. Quantas etapas, quanto tempo, o que perguntaram.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reviews de quem trabalha la.&lt;/strong&gt; Nao e nota 1 estrela com "RH e uma mae". Sao pros e contras estruturados, com nota para gestao, reconhecimento e equilibrio vida/trabalho.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hobby, nao startup
&lt;/h2&gt;

&lt;p&gt;O projeto e totalmente hobby. &lt;strong&gt;Nao tem anuncios, nao tem paywall, nao esconde informacao atras de cadastro.&lt;/strong&gt; Qualquer pessoa pode consumir tudo gratuitamente.&lt;/p&gt;

&lt;p&gt;Existe login, mas foi a forma que encontrei de evitar spam. O login e via OAuth (Google, GitHub, LinkedIn, Microsoft) e como aqui so tem dev posso garantir: &lt;strong&gt;a unica informacao que utilizo e o email, que e transformado em hash irreversivel (HMAC-SHA256) e descartado, ele nunca nem bate no banco de dados.&lt;/strong&gt; Em um login futuro é possivel criar novamente o hash e linkar ao seu user, voce continua com seu historico mesmo sem ter nenhum dado seu no banco.&lt;/p&gt;

&lt;p&gt;Traduzindo: mesmo que o banco vaze, &lt;strong&gt;nao e possivel saber de quem sao os relatos.&lt;/strong&gt; Inclusive acho bem dificil acontecer. fechei todas as portas do VPS e me conecto via Tailscale. Para servir o app, uso os tunnels da Cloudflare.&lt;/p&gt;




&lt;h2&gt;
  
  
  Status atual
&lt;/h2&gt;

&lt;p&gt;Ainda estou finalizando detalhes (a famosa versao v1.0.67-final.5).&lt;/p&gt;

&lt;p&gt;Enquanto isso, subi uma &lt;strong&gt;versao de demonstracao com dados ficticios&lt;/strong&gt; para mostrar como a plataforma funciona e ouvir os primeiros feedbacks:&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://dev.brtech.fyi/empresas/stone?tab=reviews" rel="noopener noreferrer"&gt;dev.brtech.fyi&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A versao oficial em &lt;strong&gt;&lt;a href="https://brtech.fyi" rel="noopener noreferrer"&gt;brtech.fyi&lt;/a&gt;&lt;/strong&gt; comeca vazia, com dados reais. Que durante essa postagem ainda está fechada para ajustes, mas quando vc ler pode ser que já esteja disponivel.&lt;/p&gt;

&lt;p&gt;Se a ideia fizer sentido, &lt;strong&gt;queria muito ouvir criticas e sugestoes&lt;/strong&gt; antes de abrir a versao definitiva.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;PS:&lt;/strong&gt; A plataforma pode parecer grande, mas foi tudo feito por mim. Tem termos de uso e cookies isso e gracas ao video em que o Mano Deyvin e comenta sobre compliance juridico no SaaS dele. Os termos de uso e politica de privacidade foram escritos pelo meu advogado Qwen 3.7 Plus. Tambem tem analytics nao sei nem o basico de como isso pode me ajudar, mas baseado nas tasks criadas pelos meus antigos PMs deve ser possivel identificar e melhorar o UX. &lt;strong&gt;Aceitem os cookies para me ajudar a ver como funciona isso.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Obrigado 🍻&lt;/p&gt;

</description>
      <category>career</category>
      <category>interview</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
