Privacidade e segurança
O Estudar guarda notas, planos e hábitos de estudo — dados pessoais. O desenho parte de uma regra simples: cada pessoa só vê os seus dados, e as chaves nunca chegam ao browser.
Onde ficam os dados
| Onde | O quê |
|---|---|
O teu browser (localStorage) | Uma cópia de trabalho de tudo, para a app abrir instantaneamente e funcionar offline |
Supabase (user_data) | Uma linha por utilizador com o mesmo documento, para sincronizar dispositivos |
Supabase (ai_usage) | Um contador por utilizador e por dia, só se ativares o limite |
| Fornecedor de IA | Só o que é preciso para cada pedido (ver abaixo) |
Não há analytics nem telemetria.
Quem pode entrar
Há três tipos de conta:
| Vê | Edita | Usa a IA | Vê a configuração | |
|---|---|---|---|---|
| Dono (o email do Configurar) | o seu plano e progresso | sim | sim | sim |
| Leitor | o plano e o progresso do dono | não | não | não |
| Outra conta (criada com os registos abertos) | nada | não | não | não |
- Registos fechados. Ao carregares em Configurar com o teu email, a app cria a tua conta de dono e desliga os registos novos no Supabase. Um email desconhecido recebe "Esta instalação não aceita novas contas" e nenhum email é enviado. O ponto Registos do painel de estado fica vermelho enquanto estiverem abertos.
- Leitores. Em Servidor e chaves → Login → Quem pode entrar juntas leitores pelo email; entram com o código, como tu, e veem a app em modo só leitura: sem timer, sem editar, sem IA, sem configuração.
- Remover. O botão Remover apaga a linha de leitor e a conta: o acesso acaba logo, mesmo numa sessão aberta, e os teus dados não são tocados.
Passkeys (impressão digital, rosto, PIN)
Depois de entrares com o código, a app propõe guardar uma passkey neste dispositivo. Da próxima vez carregas em Entrar com passkey e confirmas com a impressão digital, o rosto ou o PIN do dispositivo (Windows Hello, Touch ID, Face ID, Android), sem esperar pelo email. Em Conta → Passkeys vês os dispositivos registados, juntas outro e removes um que perdeste.
- A chave privada nunca sai do dispositivo (ou do gestor de palavras-passe que a sincroniza); o Supabase só guarda a parte pública. Uma passkey só funciona no endereço da app, por isso um site falso não a consegue usar.
- Só se regista uma passkey já com sessão iniciada: não abre a porta a contas novas, e um leitor removido perde o acesso com as passkeys incluídas.
- O código por email continua a ser a forma de entrar num dispositivo novo e a saída se perderes o dispositivo.
- O Publicar ativa as passkeys no Supabase para o endereço publicado. Se mudares de endereço (outro domínio), as passkeys antigas deixam de funcionar e registas de novo.
O que garante isto não é o ecrã, é o servidor:
- a base de dados só deixa o leitor ler o documento do dono, e só enquanto existir a linha em
viewers— que nenhum cliente consegue escrever; - o Worker só deixa o dono usar a IA e ver o estado do servidor;
- a configuração (chaves, publicar, acessos) só existe no servidor local (
npx estudar), que só responde a este computador — no endereço publicado essas rotas não existem, nem para o dono.
A app também recusa chaves no campo errado: a chave secreta colada no campo público seria enviada a todos os browsers, por isso nem é guardada nem é servida.
Regras de acesso (RLS)
As duas tabelas têm Row Level Security ativa. Em user_data, as quatro operações exigem auth.uid() = user_id: mesmo com a chave pública, ninguém lê ou escreve a linha de outra pessoa. Em ai_usage não há nenhuma regra para clientes — só o servidor, com a chave secreta, lhe acede. O SQL está em supabase/migrations e explicado em Modelo de dados.
Chaves
| Chave | Onde vive | Chega ao browser? |
|---|---|---|
| Supabase publishable/anon | Segredo do Worker; entregue por /api/config | Sim — foi feita para isso; a RLS protege os dados |
| Supabase secret/service_role | Segredo do Worker (.dev.vars localmente) | Nunca |
| Chave da IA | Segredo do Worker (.dev.vars localmente) | Nunca |
Token pessoal do Supabase (sbp_…) | Memória do servidor local (npx estudar), só durante a sessão | Nunca e nunca é gravado |
.dev.vars e .deploy.json ficam em ~/.estudar (ou, numa cópia do repositório, na pasta do projeto, onde estão no .gitignore). Só a pasta public/ é publicada como ficheiros estáticos, por isso um ficheiro de segredos na raiz nunca é servido.
O que a IA vê
| Pedido | Enviado |
|---|---|
| Gerar plano | Nomes e siglas das disciplinas, tipo, carga, ECTS, datas de exame, horas por dia, as tuas notas para a IA, a fatia sugerida, o domínio medido e as bases fracas |
| Ler o plano de estudos | A imagem que escolheste (reduzida para ~1800 px) |
A IA nunca recebe o teu email, os teus registos de estudo, exercícios ou respostas. O que ela devolve é tratado como não confiável: é limpo e validado (datas, notas entre 0 e 20, disciplinas existentes) antes de chegar ao ecrã, e tu confirmas antes de guardar.
Cada pedido exige uma sessão válida (o servidor confirma o token no Supabase) e conta para o limite diário, se estiver ativo.
Dispositivos partilhados
Ao terminar sessão, a app apaga a cópia local (dados, timer, sessão). Se outra pessoa entrar no mesmo dispositivo, os dados locais de quem lá estava antes nunca são juntados à conta nova.
Conteúdo escrito por ti
Nomes de disciplinas, notas e textos são sempre escapados antes de aparecerem no ecrã — um nome como <img onerror=…> aparece como texto.
Proteções técnicas
- Cabeçalhos de segurança em todas as respostas (app e API): Content-Security-Policy estrita — só scripts da própria app, sem scripts inline, sem código de terceiros (o cliente do Supabase vem incluído na app, numa versão fixa) —,
frame-ancestors 'none'contra clickjacking,nosniff,Referrer-Policy: no-referrer, HSTS e Permissions-Policy. - Dados limpos à entrada e escapados à saída. Tudo o que chega de fora (a tua linha no Supabase, a do dono para um leitor, atualizações em tempo real, cópias de segurança, a resposta da IA) passa por um filtro que só deixa os campos conhecidos, com os tipos e tamanhos certos; e todo o texto é escapado antes de ir para o ecrã.
- Sessão. O login usa PKCE: os links de email e o Google devolvem um código de uso único, não os tokens da sessão. As passkeys (WebAuthn) usam um desafio novo em cada login, verificado pelo Supabase. Terminar sessão apaga os dados deste dispositivo.
- IA. Só o dono a usa; o limite diário é reservado de forma atómica na base de dados antes de chamar o modelo; pedidos grandes (mais de 8 MB) são recusados antes de tudo.
- Servidor local. Só responde a este computador (127.0.0.1, verificação do Host contra DNS rebinding), e o ecrã de configuração exige uma chave que muda a cada arranque — vem no link que o
npx estudarabre. Uma chave guardada só é enviada para o endereço para que foi escrita: mudar o URL da IA ou do Supabase apaga-a. - Base de dados. Além das regras de acesso, as permissões (grants) retiram aos clientes tudo o que não usam, e cada documento tem um limite de 2 MB.
- Dependências. Versões fixas, GitHub Actions fixadas por commit, Dependabot semanal, e publicação no npm pela CI sem tokens, com aprovação por 2FA.
Reportar um problema de segurança
Não abras uma issue pública: usa Security → Report a vulnerability no repositório. Detalhes em SECURITY.md.