Produto independente · Mobile & Web
Kairo
App de finanças pessoais offline-first construído com Expo, React Native e TypeScript. Todos os dados financeiros ficam no dispositivo, em SQLite, o que o torna rápido, privado e independente de conexão.
- Período
- 2026 · Em desenvolvimento
- Papel
- Arquitetura, Design System e implementação
- UITelas do Expo Router compõem componentes do Design System e consomem hooks, sem SQL e sem regra de negócio.
- HooksChamam services e fazem cache com React Query, expondo estados de carregamento e erro.
- ServicesÚnico lugar com regra de negócio; TypeScript puro e com testes unitários.
- RepositoriesAcesso a dados de uma entidade, mapeando linhas para entidades de domínio tipadas; dependem apenas de um DatabaseAdapter.
- SQLiteÚnica fonte de verdade dos dados financeiros, com migrations versionadas.
Contexto
Um app financeiro precisa ser confiável antes de qualquer outra coisa: os saldos têm que fechar sempre, os dados precisam sobreviver sem conexão e o código precisa continuar manutenível à medida que cartões, parcelamentos, recorrências e sincronização chegam. A arquitetura do Kairo foi planejada em torno dessas restrições e documentada antes da implementação.
Arquitetura
Regras de negócio ficam nos services, acesso a dados nos repositories e o React apenas orquestra. Cada camada tem uma única responsabilidade e uma lista do que não pode fazer.
SQLite → Repositories → Services → Hooks → UI
Decisões principais
Dinheiro em centavos inteiros
Aritmética de ponto flutuante gera erros de arredondamento inaceitáveis em um app financeiro (0.1 + 0.2 ≠ 0.3).
Decisão: Todo valor é armazenado como INTEGER em centavos e convertido apenas para exibição, na camada de formatação.
Saldo derivado
Uma coluna de saldo armazenada dessincroniza quando transações são editadas, excluídas ou importadas.
Decisão: A conta guarda apenas o saldo inicial; o saldo atual é sempre calculado a partir das transações e mantido em cache pelo React Query, invalidado a cada mutação.
O banco como servidor local
Duplicar dados do banco em stores do cliente cria duas fontes de verdade.
Decisão: O React Query é dono de tudo que vem do SQLite; o Zustand guarda apenas estado de UI. Tratar o SQLite como "servidor local" deixa a futura sincronização quase transparente.
Pronto para sincronizar, sem implementar a sincronização
IDs auto-incrementais colidem entre dispositivos, e exclusões definitivas quebram o histórico.
Decisão: IDs UUID v4 gerados no cliente, datas ISO-8601 em UTC e soft delete com timestamps de auditoria em todas as tabelas.
Adapters em vez de remover funcionalidades
O MMKV, usado para preferências no nativo, não existe na web.
Decisão: Uma interface PreferencesStorage com implementações em MMKV e localStorage, resolvidas por plataforma (.ts / .web.ts).
Migrations versionadas
Chamadas avulsas de "CREATE TABLE IF NOT EXISTS" espalhadas pelo código tornam mudanças de schema imprevisíveis.
Decisão: Migrations sequenciais controladas por PRAGMA user_version e executadas dentro de uma transação na inicialização.
Qualidade
- TypeScript em modo strict, com validação Zod nas bordas (formulários e importação de backup).
- Hierarquia única de erros tipados (AppError → DomainError, ValidationError, NotFoundError, DatabaseError), mapeada por código na UI.
- Testes com Jest e Testing Library para services, runners de migrations e seeds, utilitários e componentes do Design System.
Status
Implementado
- Dashboard
- Contas
- Categorias
- Receitas e despesas
- Transferências
- Visualização de fluxo de caixa
Planejado
- Orçamentos
- Cartões de crédito e parcelamentos
- Transações recorrentes
- Exportação de dados
- Backup em nuvem e sincronização entre dispositivos
Stack
- Expo
- React Native
- Expo Router
- TypeScript
- SQLite
- React Query
- Zustand
- Zod
- NativeWind
- Jest
