14 KiB
CLAUDE.md - OpenMonetis
Self-hosted personal finance app (Next.js 16, React 19, PostgreSQL, Drizzle ORM, Better Auth, Tailwind 4, shadcn/ui). Portuguese UI, English folders/imports. Linter: Biome 2.x. Package manager: pnpm.
Related Projects
- OpenMonetis Companion (
~/github/openmonetis-companion): Android app que captura notificacoes de apps bancarios e envia para o OpenMonetis via API. Os itens chegam na featureinboxpara revisao.
Critical Rules
- Sempre filtrar por
userIdem queries. - Usar
getAdminPayerId(userId)desrc/shared/lib/payers/get-admin-id.tsao inves de JOIN compayerspara descobrir o admin. - Periods usam formato
YYYY-MM(ex:"2025-11"). Utils emsrc/shared/utils/period/. - Moeda: R$ com 2 decimais. DB:
numeric(12, 2). Utils emsrc/shared/utils/currency.ts. - Revalidation: usar
revalidateForEntity("entity")desrc/shared/lib/actions/helpers.tsapos mutations. - Versionamento e publicação: registrar mudancas no
CHANGELOG.mdseguindo Keep a Changelog, também alterar opackage.jsone o badge de versão doREADME.md. Cada versão deve ter um parágrafo introdutório em linguagem humana logo abaixo do cabeçalho## [x.y.z], antes das seções### Adicionado/Alterado/Removido— descrevendo em prosa o que a versão representa (ex: "Esta versão foca em polimento visual e reorganização interna..."). Amainexecuta somente a CI; imagens Docker e GitHub Releases são publicadas exclusivamente por tags SemVer no formatovX.Y.Z. Antes de criar a tag, confirmar que a CI damainpassou e que tag,package.json,CHANGELOG.mde badge doREADME.mdusam a mesma versão. A tag deve apontar para o commit validado. Criar ou enviar uma tag dispara publicação externa (X.Y.Z,X.Y,Xelatestno Docker Hub, seguida da GitHub Release), portanto agentes nunca devem criar ou fazer push de tags sem autorização explícita do usuário. Quando o usuário pedir "commit e push" em uma mudança que prepara uma nova versão, isso conta como autorização explícita para executar o fluxo completo: commitar, enviar amain, aguardar a CI damainpassar, criar a tag SemVer correspondente à versão preparada e enviar essa tag. Se não houver versão preparada ou se houver divergência entrepackage.json,CHANGELOG.md, badge doREADME.mde tag pretendida, parar e pedir confirmação. Não voltar a publicarlatestdiretamente de pushes namain. - Comunicacao: responder em portugues clara e direta com o time.
- Commit messages: agrupar por natureza. em pt-br. seguindo o padrao do sistema.
- README.md: sempre que fizer alteracoes significativas, atualize o README.md.
Architecture
Feature-First
src/app/: roteamento, layouts, loading states e paginas finassrc/features/: codigo de dominio por featuresrc/shared/: tudo que e genuinamente reutilizado entre featuressrc/db/: schema do banco
Regra Feature vs Shared
Use esta pergunta:
Se eu deletar esta feature, este arquivo deveria sumir junto?
- Sim: vai para
src/features/<feature>/ - Nao: vai para
src/shared/
Features nao importam outras features
Se um contrato cruza dominios, ele deve morar em src/shared/.
Excecao intencional: attachments depende de transactions
src/features/attachments importa TransactionDialog, TransactionDetailsDialog e TransactionItem diretamente de src/features/transactions. Isso e uma dependencia explicita e aceita: anexos sao semanticamente uma extensao de lancamentos — existem por causa deles e nao fazem sentido sem esse contexto. Mover esses componentes para shared/ seria errado (eles pertencem a transactions). Nao tratar isso como bug a corrigir.
Exemplos comuns:
- auth:
src/shared/lib/auth/* - db:
src/shared/lib/db.ts - revalidation helpers:
src/shared/lib/actions/* - payers cross-domain helpers:
src/shared/lib/payers/* - period/currency/date:
src/shared/utils/* - shadcn/ui:
src/shared/components/ui/*
Directory Structure
src/
├── app/
│ ├── (auth)/
│ │ ├── login/page.tsx
│ │ └── signup/page.tsx
│ ├── (dashboard)/
│ │ ├── dashboard/
│ │ ├── transactions/
│ │ ├── cards/
│ │ │ └── [cardId]/invoice/
│ │ ├── accounts/
│ │ │ └── [accountId]/statement/
│ │ ├── categories/
│ │ │ ├── [categoryId]/
│ │ │ └── history/
│ │ ├── budgets/
│ │ ├── payers/
│ │ │ └── [payerId]/
│ │ ├── notes/
│ │ ├── insights/
│ │ ├── calendar/
│ │ ├── inbox/
│ │ ├── attachments/
│ │ ├── changelog/
│ │ ├── reports/
│ │ │ ├── category-trends/
│ │ │ ├── card-usage/
│ │ │ ├── installment-analysis/
│ │ │ └── establishments/
│ │ └── settings/
│ ├── (landing-page)/
│ ├── api/
│ ├── globals.css
│ └── layout.tsx
├── features/ # cada feature segue: actions.ts, queries.ts, actions/, components/, hooks/, lib/
│ ├── auth/
│ ├── landing/
│ ├── dashboard/
│ ├── transactions/
│ ├── cards/
│ ├── invoices/
│ ├── accounts/
│ ├── categories/
│ ├── budgets/
│ ├── payers/
│ ├── notes/
│ ├── insights/
│ ├── calendar/
│ ├── inbox/
│ ├── attachments/
│ ├── reports/
│ └── settings/
├── shared/
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives
│ │ ├── navigation/ # navbar, sidebar, breadcrumbs
│ │ ├── providers/ # React context providers
│ │ ├── brand/ # logos do app (logo, logo-icon, logo-text)
│ │ ├── widgets/ # widget-card, widget-empty-state, expandable-widget-card
│ │ ├── feedback/ # empty-state, status-dot, payment-success
│ │ ├── month-picker/
│ │ ├── logo-picker/
│ │ ├── calculator/
│ │ ├── entity-avatar/
│ │ └── skeletons/
│ ├── hooks/
│ ├── lib/
│ │ ├── actions/
│ │ ├── auth/
│ │ ├── accounts/
│ │ ├── cards/
│ │ ├── calculator/
│ │ ├── categories/
│ │ ├── email/
│ │ ├── import/
│ │ ├── installments/
│ │ ├── invoices/
│ │ ├── logo/
│ │ ├── notifications/
│ │ ├── payers/
│ │ ├── schemas/
│ │ ├── storage/
│ │ ├── transfers/
│ │ ├── types/
│ │ ├── version/
│ │ └── db.ts
│ └── utils/
│ ├── period/
│ ├── calculator.ts
│ ├── calendar.ts
│ ├── category-colors.ts
│ ├── currency.ts
│ ├── date.ts
│ ├── export-branding.ts
│ ├── fetch-json.ts
│ ├── financial-dates.ts
│ ├── icons.tsx
│ ├── id.ts
│ ├── initials.ts
│ ├── math.ts
│ ├── number.ts
│ ├── percentage.ts
│ ├── string.ts
│ └── ui.ts
└── db/
└── schema.ts
Estrutura interna padrão de uma feature
Toda feature em src/features/<nome>/ segue:
<feature>/
├── actions.ts # entry point de Server Actions (barrel quando há actions/)
├── queries.ts # entry point de leitura do banco
├── actions/ # (opcional) Server Actions divididas por domínio quando o volume cresce
├── components/ # componentes de UI da feature
├── hooks/ # React hooks específicos da feature
└── lib/ # helpers, types, sub-queries e constantes internas
actions.ts e queries.ts são as portas de entrada da feature. Tudo que é helper interno fica em lib/. Componentes e hooks ficam nas pastas com nome óbvio.
Import Patterns
Preferidos
import { getUser } from "@/shared/lib/auth/server";
import { revalidateForEntity } from "@/shared/lib/actions/helpers";
import { parsePeriodParam } from "@/shared/utils/period";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import { fetchLancamentos } from "@/features/transactions/queries";
Evitar
import { Something } from "@/components/...";
import { Something } from "@/lib/...";
import { something } from "@/app/(dashboard)/...";
App Router Pattern
Paginas em src/app/ devem ser finas:
import { getUser } from "@/shared/lib/auth/server";
import { TransactionsPage } from "@/features/transactions/components/page/transactions-page";
import { fetchLancamentos } from "@/features/transactions/queries";
export default async function Page() {
const user = await getUser();
const data = await fetchLancamentos([/* filters */]);
return <TransactionsPage {...data} />;
}
Layouts, loading.tsx e metadata continuam em src/app/.
Naming
Routes / folders
| Portugues | English |
|---|---|
lancamentos |
transactions |
cartoes |
cards |
contas |
accounts |
categorias |
categories |
orcamentos |
budgets |
pessoas |
payers |
Nota: o conceito de "pagador" foi renomeado para "pessoa" na UI (labels, toasts, textos visíveis ao usuário). O código, rotas e schema continuam usando o termo original em inglês (
payer,payerId,adminPayerId) e em português interno (pagadorcomo variável). Não renomear esses identificadores — a divergência entre UI e código é intencional e documentada. |anotacoes|notes| |calendario|calendar| |ajustes|settings| |pre-lancamentos|inbox| |relatorios/tendencias|reports/category-trends| |relatorios/uso-cartoes|reports/card-usage| |relatorios/analise-parcelas|reports/installment-analysis| |relatorios/estabelecimentos|reports/establishments| |contas/[contaId]/extrato|accounts/[accountId]/statement| |cartoes/[cartaoId]/fatura|cards/[cardId]/invoice| |categorias/historico|categories/history| |changelog|settings/changelog|
Files
- preferir
kebab-case - preferir nomes em ingles
- manter nomes internos de tipos/funcoes somente quando a troca aumentar risco sem ganho real
Commands
pnpm run dev
pnpm run build
pnpm run lint
pnpm run lint:fix
pnpm exec next typegen
pnpm exec tsc --noEmit
pnpm run db:generate
pnpm run db:push
pnpm run db:studio
pnpm run docker:up:db
Revalidation
Arquivo: src/shared/lib/actions/helpers.ts
- atualizar sempre os paths em ingles
- lembrar de manter a tag
"dashboard"para invalidacoes financeiras
Auth
getUser()/getUserId()emsrc/shared/lib/auth/server.ts- sessao deduplicada por request com
React.cache()
Dashboard Fetcher
Padrao recomendado:
import { getAdminPayerId } from "@/shared/lib/payers/get-admin-id";
export async function fetchData(userId: string, period: string) {
const adminPayerId = await getAdminPayerId(userId);
if (!adminPayerId) return [];
return db.query.transactions.findMany({
where: /* sempre com userId + adminPayerId + period */,
});
}
New Feature Checklist
- Criar a rota fina em
src/app/(dashboard)/<feature>/page.tsx - Criar a feature em
src/features/<feature>/ - Separar:
components/queries.ts(entry point de leitura)actions.ts(entry point de Server Actions; vira barrel quando crescer e migrar paraactions/)lib/para helpers internos, sub-queries por tópico, types e constantes da featuretypes.tsouschemas.tsquando fizer sentido (alternativa alib/)hooks/quando houver hooks específicos da feature
- Extrair para
src/shared/tudo que for reutilizavel - Atualizar navegacao e
revalidateForEntity()se a feature tiver CRUD - Rodar:
pnpm exec next typegenpnpm exec tsc --noEmitpnpm run lint
Security Rules
Regras aplicadas automaticamente ao gerar codigo.
Secrets
Nunca colocar API keys, credenciais de banco ou tokens em codigo frontend. Evitar variaveis prefixadas com NEXT_PUBLIC_ para dados sensiveis — estas sao bundladas no cliente. Usar variaveis server-side apenas. .env deve estar no .gitignore antes do primeiro commit. .env.example deve ter apenas placeholders.
Autenticacao & Autorizacao
Toda rota protegida em src/app/api/ requer getUser() ou getOptionalUserSession() antes de qualquer logica, retornando 401 para nao autenticados. Rotas com IDs de recursos devem verificar ownership: eq(table.userId, userId). Rotas admin devem checar role e retornar 403 para nao-admins. Session cookies em Better Auth ja tem httpOnly, secure e sameSite configurados — nao alterar.
Input & Output
Usar Drizzle ORM (parametrizado por padrao) — nunca concatenar input de usuario em SQL. Validar todo input com Zod antes de usar. Upload de arquivos: usar whitelist de MIME types (ALLOWED_MIME_TYPES), presigned URLs para S3, token de upload assinado com verificacao pos-upload. Nunca usar dangerouslySetInnerHTML com conteudo de usuario.
Headers & CSP
CSP definida em src/proxy.ts via middleware — alterar la, nao em next.config.ts. Headers de seguranca (HSTS, X-Frame-Options, etc.) definidos em next.config.ts. Nao remover nem enfraquecer essas configuracoes.
Rate Limiting
Login: 5 tentativas/min. Signup: 3 tentativas/min. API tokens: 100 req/min (inbox), 20 req/min (batch). Configurado em src/shared/lib/auth/config.ts e nas rotas de inbox. Nao remover.
Tratamento de Erros
Erros nao devem expor stack traces, paths ou nomes de bibliotecas ao cliente. Usar mensagens genericas: "Algo deu errado". Logar detalhes apenas no servidor com console.error().
Dependencias
Verificar pacotes novos sugeridos pela IA em npmjs.com antes de instalar. Red flags: menos de 1.000 downloads/semana, publicado nos ultimos 30 dias, nome muito parecido com pacote popular. Rodar pnpm audit periodicamente.