Projeto real
Community Split — Contas da casa sem drama
App para quem divide casa: rateia as contas em centavos exatos, fecha o mês e transforma uma teia de dívidas no menor número possível de Pix.

- Pix, no máximo, para quitar uma casa de N pessoas
- N − 1
- modos de rateio: igual, por cotas e por valores
- 3
- perdidos em arredondamento — tudo em centavos inteiros
- R$ 0,00
- testes automatizados entre API e interface
- 85
- Ano
- 2026
- Papel
- Produto & full-stack
- Cliente
- Projeto próprio
- Tecnologias
- Next.js
- React
- TypeScript
- Fastify
- PostgreSQL
- Drizzle ORM
- Zod
- Tailwind CSS
- Radix UI
- Docker
- MCP
Quem já morou em república conhece a cena: a Ana paga o mercado, o Bê paga a internet, o Carlão paga a luz — e no fim do mês ninguém sabe quem deve quanto para quem. Cobrar R$ 15 de um amigo é constrangedor, a planilha fica desatualizada e a louça acumula na pia.
O Community Split é o árbitro neutro da casa. Cada morador lança o que pagou, o sistema divide de forma justa, consolida os saldos e diz exatamente quais Pix fazer — já com QR Code e Copia e Cola. De quebra, as tarefas domésticas viram um placar com pontos.
Fiz o produto de ponta a ponta: regras de negócio, API, banco de dados, interface e deploy. Abaixo, a jornada real de um morador, com prints tirados do app rodando, e o que acontece por trás de cada tela.
1. Entrar na casa
A jornada começa num link de convite enviado pelo WhatsApp. Cada casa tem um código curto (ex.: REP-4921). Quem recebe cria a conta — nome, e-mail, senha e, opcionalmente, a chave Pix onde quer receber — e envia um pedido para entrar.
Ninguém entra sozinho: o pedido fica pendente até um administrador aprovar. Assim um link vazado não coloca um estranho dentro das finanças da casa, e o admin pode renovar o código a qualquer momento, invalidando o anterior.
- Senha com 8 a 128 caracteres, maiúscula, minúscula, número e símbolo; guardada com scrypt e sal aleatório.
- Sessão em cookies HttpOnly: access token curto + refresh token de 30 dias, salvo só como hash SHA-256 e rotacionado a cada uso.
- Papéis por casa (administrador ou morador): a mesma conta pode morar em várias casas.



2. Lançar uma despesa (e dividir certo)
Lançar uma conta leva segundos: título, valor, categoria, data e quem participa. A parte interessante é o rateio, que tem três modos — igualitário, por cotas (pesos) e por valores definidos à mão.
O detalhe que separa um app sério de uma calculadora é o centavo. R$ 100 divididos por 3 dá 33,333…, e alguém precisa ficar com o centavo que sobra. O Community Split converte tudo para centavos inteiros, divide, e distribui as sobras de forma determinística: no modo igualitário, para os primeiros participantes pela ordem do ID; no modo por cotas, pelo método do maior resto. O resultado sempre fecha exatamente com o total — nunca R$ 99,99, nunca R$ 100,01.
No print abaixo, o churrasco de R$ 250 foi dividido em 8 cotas: quem levou convidados pesa mais. O Lucas, com 3 cotas, paga R$ 93,75; quem tem 1 cota paga R$ 31,25.
- Valores com mais de duas casas decimais são rejeitados antes de qualquer conta.
- No modo por valores, a soma informada precisa bater exatamente com o total.
- Conta “pendente” (ainda não paga ao fornecedor) não gera dívida entre moradores; só passa a valer quando alguém registra o pagamento.
- Somente o autor ou um admin edita a despesa, e cada edição incrementa uma versão: se duas pessoas editam ao mesmo tempo, a segunda recebe 409 e um aviso para atualizar a página.
// R$ 100,00 ÷ 3 → [33,34, 33,33, 33,33]
const base = Math.floor(totalCentavos / participantes.length);
const centavosRestantes = totalCentavos % participantes.length;
const idsComCentavoExtra = new Set(
participantes
.map(({ membroId }) => membroId)
.sort(compararMembroId)
.slice(0, centavosRestantes),
);
return participantes.map(({ membroId }) => ({
membroId,
valorAtribuido: (base + (idsComCentavoExtra.has(membroId) ? 1 : 0)) / 100,
}));

3. Fechar o mês
Se os saldos mudassem a cada lançamento, ninguém saberia quando pagar: você faz o Pix e, cinco minutos depois, alguém lança mais uma conta. A solução foi trazer para o app um ritual que as casas já fazem: o fechamento do mês.
Enquanto o mês está aberto, as despesas aparecem e podem ser corrigidas, mas não geram Pix. Quando o administrador fecha o mês, as contas dele ficam congeladas e entram nos saldos. Só então os acertos são sugeridos.
- Meses fecham em ordem; meses vazios no caminho fecham juntos, e um mês anterior com despesas ainda aberto bloqueia o fechamento.
- Contas pendentes bloqueiam o fechamento — ou são movidas para o dia 1º do mês seguinte, se o admin escolher.
- Mês fechado não aceita lançar, editar, pagar nem excluir despesas; só trocar a categoria, que não mexe em valores.
- Só o último mês fechado pode ser reaberto, e apenas se nenhum Pix tiver sido confirmado depois do fechamento (esse Pix pode ter usado aqueles saldos).
- Fechar e reabrir travam a casa com SELECT … FOR UPDATE; alterações em despesas pegam a mesma trava em modo compartilhado, então nada escapa no meio do caminho.

4. Menos Pix: simplificando as dívidas
Em outubro, a casa teve 4 contas pagas por 3 pessoas diferentes, divididas entre 4 moradores. Se cada um pagasse sua parte de cada conta para quem pagou, seriam até 12 transferências. O Community Split resolve com 3.
O truque é olhar para o saldo líquido de cada pessoa: quanto pagou menos quanto consumiu. A Ana pagou R$ 570 e consumiu R$ 232,50, então tem +R$ 337,50 a receber. A Dani não pagou nada e consumiu R$ 232,50: deve −R$ 232,50. Somando todos os saldos, o resultado é sempre zero — o dinheiro só muda de mão.
Com os saldos em mãos, um algoritmo guloso de dois ponteiros ordena devedores e credores do maior para o menor e casa os dois do topo: transfere o menor dos dois valores, zera um deles e avança. Cada passo zera pelo menos uma pessoa, então uma casa com N moradores nunca precisa de mais de N − 1 Pix.
credores.sort((a, b) => b.saldoEmCentavos - a.saldoEmCentavos);
devedores.sort((a, b) => b.saldoEmCentavos - a.saldoEmCentavos);
while (devedorIndex < devedores.length && credorIndex < credores.length) {
const devedor = devedores[devedorIndex];
const credor = credores[credorIndex];
const valorCentavos = Math.min(devedor.saldoEmCentavos, credor.saldoEmCentavos);
transacoes.push({ devedor, credor, valor: valorCentavos / 100 });
devedor.saldoEmCentavos -= valorCentavos;
credor.saldoEmCentavos -= valorCentavos;
if (devedor.saldoEmCentavos === 0) devedorIndex++;
if (credor.saldoEmCentavos === 0) credorIndex++;
}
5. Pagar com Pix e confirmar
Na tela de acertos, a Dani vê só o que lhe interessa: “pague R$ 232,50 para a Ana”. Ao clicar em Pagar Pix, o app monta o BR Code — o padrão EMV do Banco Central por trás de todo QR Code Pix — com a chave da Ana e o valor exato. A Dani escaneia com o app do banco ou usa o Copia e Cola.
O BR Code é uma sequência de campos no formato ID + tamanho + valor (TLV), terminada por um CRC16-CCITT que o banco usa para validar o código. Gerar isso sem biblioteca exigiu entender a especificação: nomes sem acento, cidade com até 15 caracteres e o checksum calculado sobre o próprio payload.
O dinheiro nunca passa pelo app. Por isso a quitação tem duas etapas: quem pagou informa o Pix, e quem recebeu confirma. Só a confirmação do credor abate os saldos — e o devedor pode cancelar enquanto ela não acontece.
const emvField = (id: string, value: string) =>
`${id}${value.length.toString().padStart(2, "0")}${value}`;
let payload =
emvField("00", "01") + // formato do payload
emvField("26", emvField("00", "br.gov.bcb.pix") + emvField("01", chave)) +
emvField("53", "986") + // moeda: real
emvField("54", valor.toFixed(2)) + // valor exato
emvField("58", "BR") + emvField("59", nome) + emvField("60", cidade);
return `${payload}6304${crc16(`${payload}6304`)}`;


6. Tarefas que viram placar
Dinheiro não é a única fonte de atrito. Cada tarefa da casa tem responsável, prazo, frequência (diária a avulsa) e um peso em pontos — de 10 para algo simples a 30 para uma faxina geral. Ao concluir, os pontos vão para o responsável e o ranking do mês atualiza na hora.
A ideia é trocar a bronca pelo reconhecimento: em vez de cobrar quem não lavou a louça, a casa vê quem mais colaborou. Só o responsável ou um admin pode marcar a tarefa como feita, e cada conclusão fica no histórico.

Por trás das telas
O front é um app Next.js com React, Tailwind e componentes Radix (no estilo shadcn/ui), com gráficos em Recharts e micro-interações em Framer Motion. Ele não fala direto com a API: um proxy em /api encaminha as chamadas para o backend na rede interna, reescrevendo os cookies para o domínio público. O navegador só conhece uma origem, sem CORS e sem token exposto ao JavaScript.
A API é Fastify com TypeScript. Toda entrada passa por schemas Zod, o banco é PostgreSQL acessado via Drizzle ORM, e as migrações rodam sozinhas antes do servidor subir. A documentação OpenAPI/Swagger é gerada a partir das rotas.
As regras de negócio vivem em funções puras (rateio, simplificação, fechamento, permissões, Pix), separadas do acesso ao banco. Isso deixa o núcleo fácil de testar: 58 testes no backend — unitários e de integração contra um Postgres real, incluindo concorrência de pagamento e edição — e mais 27 no front.
Um extra que gostei de construir: a API também é um servidor MCP (Model Context Protocol), com OAuth 2.1 e PKCE. Assistentes de IA, como o Claude, podem listar casas, lançar despesas, pagar contas e concluir tarefas em nome do usuário, limitados aos escopos que ele autorizou. Tudo roda em Docker, publicado via Dokploy.
- Dinheiro sempre em centavos inteiros; reais só na borda, para exibir.
- Concorrência tratada no banco: travas de linha nas operações financeiras e versão otimista nas edições.
- Erros de domínio com status HTTP certos (400, 403, 404, 409) e mensagens em português para o usuário final.