Pular para o conteúdo

Todos os trabalhosMindy

O dado de um na tela do outro.

App brasileiro para casa compartilhada: o que é da casa aparece para todos, o que é pessoal fica privado. Sou cofundador e único desenvolvedor; meu sócio cuida de marca e comercial. Construí entre julho e outubro de 2026 e mantive em beta privado por convite até a pausa. Está pausado por decisão minha enquanto foco em outros projetos. O código é privado, e o que mostro aqui são as partes que consigo demonstrar sem expor nada.

Estado
beta privado, pausado
Período
07 a 10/2026
Stack
Flutter e Dart, Supabase (Auth, Postgres, Edge Functions, pg_cron, pg_net), Web Push e FCM, pgTAP, GitHub Actions

Feito por mim

Produto, aplicativo, banco de dados, funções de borda, testes e CI.

Contexto

Produto próprio. Marca e comercial com meu sócio.

Por dentro

A regra mora no banco

Registro privado tem dono; registro compartilhado tem casa. A leitura da casa passa por uma função que confere se você é membro ativo agora. Sair da casa corta o acesso na hora. Os testes assumem a identidade de duas pessoas em sequência e plantam "canários" pessoais para conferir que continuam privados depois de elas entrarem na mesma casa.

SQL
create function public.is_own_active_house_member(p_house_id uuid)
returns boolean language sql stable security definer set search_path = '' as $$
  select exists (
    select 1 from public.house_members as member
    where member.house_id = p_house_id
      and member.user_id = auth.uid()
      and member.left_at is null);
$$;

create policy house_tasks_select_active_member on public.house_tasks
  for select to authenticated
  using (public.is_own_active_house_member(house_id));

Web Push a partir das RFCs

A biblioteca padrão depende de APIs do Node que a função de borda não tem. A cifra (RFC 8291 e 8188) e o token VAPID (RFC 8292) usam só WebCrypto, em 368 linhas, e conferi contra o exemplo oficial da RFC, byte a byte. Uma auditoria depois achou o risco que faltava: o endereço de push mora numa tabela que o usuário escreve, então uma inscrição forjada fazia a função chamar qualquer host. Hoje só vão os hosts permitidos.

TypeScript
// RFC 8291 §3.3-3.4: IKM a partir do segredo ECDH e do auth do navegador
const keyInfo = concat(encoder.encode('WebPush: info\0'), uaPublicRaw, localPublicRaw);
const ikm = await hkdf(authSecret, sharedSecret, keyInfo, 32);

// RFC 8188 §2.2: chave e nonce do GCM
const contentKey = await hkdf(salt, ikm, encoder.encode('Content-Encoding: aes128gcm\0'), 16);
const nonce      = await hkdf(salt, ikm, encoder.encode('Content-Encoding: nonce\0'), 12);

// Um registro só, e o último: delimitador 0x02
const record = concat(plaintext, new Uint8Array([0x02]));

A fila é a garantia, a chamada é a pressa

Criar uma tarefa grava o aviso numa fila no banco. A chamada imediata é só pressa e pode falhar à vontade: a exceção é engolida de propósito. Um cron a cada 2 minutos reentrega o que parou, até 6 tentativas. E o recibo separa aceito, mostrado e clicado, porque "o serviço aceitou" não é "o aparelho mostrou".

A hora que não existe

"Toda segunda às 8h" não é "a cada 24 horas". O banco percorre datas civis no fuso da rotina e recusa hora que não existe no salto do horário de verão, com ida e volta: se a conversão não devolve a hora pedida, levanta erro. O relógio de teste só aceita falsificação quando o usuário é o dono do banco e a chave de teste está ligada.

SQL
requested_local_timestamp := p_local_date + p_local_time;
scheduled_instant := requested_local_timestamp at time zone p_timezone;
-- O Postgres normaliza hora de salto de DST para outra hora local. Rejeitar a perda.
if scheduled_instant at time zone p_timezone
   is distinct from requested_local_timestamp then
  raise exception using errcode = '22023',
    message = 'Local time does not exist in timezone';
end if;

O juiz não pode morar no aparelho

O limite diário de IA era debitado antes da chamada paga, e o estorno estava liberado ao token da própria pessoa: qualquer conta zerava o contador. Uma auditoria achou isso por dois caminhos. Agora o consumo deixa uma reserva e o estorno é executável só pela função de borda, uma vez, dentro de 15 minutos, e só quando o modelo não respondeu.

A IA não tem caneta

O modelo interpreta texto livre e devolve uma estrutura. Quem grava é sempre uma função versionada no banco, com um identificador de pedido estável: toque duplo cria uma tarefa só. Ações destrutivas ou ambíguas pedem confirmação; criações simples por voz saem com recibo e Desfazer. Sem rede ou sem cota, cai num interpretador local de custo zero.

Como sei que funciona

A última execução do CI, em 12/10/2026, passou 3.159 testes de Dart (7 pulados), 1.782 asserções pgTAP em 75 arquivos e 82 testes das funções de borda, além de busca por segredos, guardas estáticas de SQL e aceitação em emulador Android em duas versões da API.

O que isso não faz

  • Está pausado: o último commit é de 12/10/2026.
  • Não está aberto ao público: é beta privado por convite.
  • A guarda estática não vê se a policy isola as linhas certas, e diz isso na própria saída. Quem confere isso é o pgTAP.

Telas

Tela Hoje da Mindy, com a agenda do dia e dados de exemplo
Hoje, com dados de exemplo.
Lista de compras compartilhada na Mindy, com dados de exemplo
A lista que a casa toda vê.

O que quebrou

  1. 42501 permission denied for schema private.

    Gatilho adiado roda fora da função com permissão; os testes terminam em rollback e nunca o disparavam. Agora roda como dono.

  2. HTTP 2xx, e nada na tela do iPhone.

    Inscrições órfãs. Recibo em três camadas e faxina.

  3. otp_expired.

    O robô do provedor gastava o link. Agora o token é trocado por POST, na minha página.

Créditos: Produto próprio, com meu sócio em marca e comercial. Código privado.

Me conta o problema.

Uma mensagem basta para começar. Eu mesmo respondo e, se fizer sentido, mando uma proposta por escrito: o que entra, o que fica de fora, prazo e preço.

WhatsApp (11) 99227-9903