Pular para conteúdo

Arquitetura de Software — Decisões ADRs

Versão: 1.0 Escopo: Documento único contendo todos os ADRs do projeto.

ADR-001 — Estilo Arquitetural: Clean Architecture + DDD em Módulos NestJS

  • Decisão: Estruturar o backend NestJS em camadas explícitas por módulo de domínio (Bounded Context): domain (entidades, value objects, regras de negócio, interfaces de repositório), application (use cases orquestrando regras), infrastructure (implementações Prisma dos repositórios, adapters externos) e interface (controllers, DTOs, guards). Módulos NestJS mapeiam Bounded Contexts (Auth, Cadernos, Itens, Calendário, Busca).
  • Por quê: Isola regras de negócio (RF-010, RF-011, RF-014, etc.) de detalhes de infraestrutura (Prisma, NestJS), permitindo testar o domínio com Jest puro, sem banco.
  • Trade-off: Mais boilerplate (interfaces, mappers entre entidade de domínio e modelo Prisma) do que um CRUD direto sobre o Prisma; aceitável dado o volume de regras de negócio não triviais do sistema.
  • < Backward: Especificação (blocos 1 e 2), G-000, G-014
  • > Forward: Container: API Backend (NestJS) — C4 Nível 2; TASK-001 a TASK-070 (organização interna)

ADR-002 — Organização de Repositório: Monorepo

  • Decisão: Utilizar um monorepo único contendo o backend NestJS (Backend) e o frontend Next.js (Frontend), com pacote compartilhado (shared) para tipos e DTOs comuns.
  • Por quê: Projeto de um único desenvolvedor; monorepo evita duplicação de tipos entre frontend e backend e simplifica versionamento conjunto no MVP.
  • Trade-off: Build/CI levemente mais complexo (workspaces), porém desproporcionalmente menor que o custo de manter dois repositórios sincronizados manualmente.
  • < Backward: Especificação (Visão Geral, blocos 1 e 2)
  • > Forward: Container: Frontend Web (Next.js), Container: API Backend (NestJS) — C4 Nível 2

ADR-003 — Prisma como Adapter de Persistência (Ports & Adapters)

  • Decisão: O Prisma Client é usado exclusivamente na camada infrastructure, implementando interfaces de repositório (ItemRepository, NotebookRepository, etc.) definidas na camada domain. Nenhuma camada domain/application importa @prisma/client diretamente.
  • Por quê: Mantém o domínio agnóstico ao ORM, permitindo testar regras de negócio (ciclo de vida, recorrência, deleção) sem subir banco de dados.
  • Trade-off: Necessário mapear manualmente entre o modelo Prisma e as entidades de domínio (mappers), aumentando código boilerplate.
  • < Backward: ASR-009, ASR-010
  • > Forward: Container: API Backend (NestJS), Container: Banco de Dados (PostgreSQL); TASK-001, TASK-010, TASK-015

ADR-004 — Modelagem de Item: Single Table Inheritance no Prisma

  • Decisão: Usar uma única tabela items com coluna discriminadora type (SCHEDULE | TODO) e campos específicos por tipo como colunas nulináveis (ex.: startAt/endAt obrigatórios só para SCHEDULE; dueDate, isCompleted só para TODO).
  • Por quê: Simplifica os joins exigidos pelas visões unificadas (Calendário, Planner, Busca, Todo List), que consultam SCHEDULE e TODO na mesma query.
  • Trade-off: Colunas nulas para o tipo que não as usa; obrigatoriedade condicional (ex.: datas de SCHEDULE) precisa ser validada na camada de domínio, não pelo schema do banco.
  • < Backward: ASR-010, RF-005, RF-006
  • > Forward: TASK-010, TASK-012, TASK-034, TASK-041

ADR-005 — Hashing de Senha com Argon2id

  • Decisão: Usar a biblioteca argon2 para hash de senha, com parâmetros de custo padrão recomendados e salt automático por chamada.
  • Por quê: Argon2id é o algoritmo atualmente recomendado (vencedor da Password Hashing Competition), superior a bcrypt em resistência a ataques com hardware dedicado (GPU/ASIC).
  • Trade-off: Dependência nativa (binding C++) exige build compatível no Docker/CI; mitigado usando imagem Docker Node oficial com toolchain já configurada.
  • < Backward: ASR-001, RF-017, RF-018
  • > Forward: TASK-061 (registro), TASK-064 (troca de senha)

ADR-006 — Autenticação via Passport (JWT Strategy) + Refresh Token Custom

  • Decisão: Usar @nestjs/passport com passport-jwt para validar o Access Token nas rotas protegidas; o Refresh Token é implementado manualmente (fora do Passport) como fluxo custom de rotação, com hash do token opaco persistido via Prisma na tabela sessions.
  • Por quê: passport-jwt é a integração padrão do ecossistema NestJS para Access Token stateless; o Refresh Token, por ser opaco e persistido, não se encaixa no modelo de strategy do Passport e é mais simples de controlar manualmente (revogação individual/massa).
  • Trade-off: Dois mecanismos de “autenticação” coexistindo (Passport para access, service custom para refresh) aumenta levemente a superfície de manutenção comparado a uma biblioteca de sessão única.
  • < Backward: ASR-003, RF-021
  • > Forward: TASK-050, TASK-055, TASK-057

ADR-007 — Revogação de Sessão Centralizada em Função Única

  • Decisão: Implementar uma única função revokeAllSessions(userId, exceptSessionId?) no módulo de Sessão, reutilizada tanto pela troca de senha (RF-024, preserva a sessão corrente) quanto pela desativação de conta (RF-022, revoga todas sem exceção).
  • Por quê: A única diferença de negócio entre os dois fluxos é o parâmetro opcional exceptSessionId; implementações separadas arriscariam divergir com o tempo.
  • Trade-off: Nenhum trade-off relevante — decisão de baixo custo com alto ganho de consistência, registrada para documentar explicitamente o contrato compartilhado.
  • < Backward: ASR-005, RF-022, RF-024
  • > Forward: TASK-058, TASK-066, TASK-067

ADR-008 — Guard de Autenticação Global via APP_GUARD

  • Decisão: Registrar um AuthGuard como provider global (APP_GUARD) no módulo raiz do NestJS, protegendo todas as rotas por padrão; rotas públicas (login, registro) são explicitamente marcadas com um decorator @Public().
  • Por quê: Garante “secure by default” — nenhum novo endpoint de Cadernos/Itens/Sub-itens fica desprotegido por esquecimento; a exceção precisa ser explícita.
  • Trade-off: Todo endpoint verdadeiramente público exige o decorator explícito, adicionando uma etapa de atenção no desenvolvimento.
  • < Backward: ASR-004, RNF-002
  • > Forward: Container: API Backend (NestJS) — transversal a TASK-001 a TASK-070

ADR-009 — Account Lockout: Contador em Banco (conta) + NestJS Throttler (IP)

  • Decisão: Bloqueio por conta (5 tentativas falhas) é controlado por colunas failedLoginAttempts/lockedUntil na tabela de usuário via Prisma; complementarmente, @nestjs/throttler limita a taxa de requisições por IP no endpoint de login.
  • Por quê: O contador por conta é regra de negócio (RF-019) e precisa sobreviver a reinícios, por isso vai ao banco; o Throttler por IP é proteção de infraestrutura complementar, adequada em memória para instância única do MVP.
  • Trade-off: Throttler em memória não escalaria para múltiplas instâncias sem store compartilhado (Redis) — aceito como dívida técnica nesta versão.
  • < Backward: ASR-002, RF-019
  • > Forward: TASK-053, TASK-054

ADR-010 — Deleção com Efeitos em Cascata via Transação Prisma

  • Decisão: Exclusão de conta usa prisma.$transaction com onDelete: Cascade no schema para Cadernos/Itens/Sub-itens; exclusão de Caderno usa prisma.$transaction com atualização explícita (notebookId: null) dos itens antes de remover o Caderno — nunca cascade automático nesse caso.
  • Por quê: Os dois fluxos têm efeitos opostos (cascata total vs. preservação movendo para Inbox) e precisam de garantia transacional para não deixar dados órfãos em caso de falha parcial.
  • Trade-off: Cascade automático do Prisma não pode ser usado para Caderno → Item (anularia a regra de mover para Inbox), exigindo lógica de aplicação explícita nesse caso específico.
  • < Backward: ASR-006, RF-011, RF-023
  • > Forward: TASK-007, TASK-069

ADR-011 — Reativação de Conta como Resposta de Login Diferenciada

  • Decisão: POST /api/auth/login retorna status: "requires_reactivation" (HTTP 403) quando a conta está Desativada e as credenciais são válidas, em vez de emitir tokens ou erro genérico. A confirmação aciona um endpoint separado (POST /api/user/reactivate), que só então libera o login completo.
  • Por quê: Separar “checar credenciais” de “confirmar reativação” evita reativar a conta silenciosamente sem consentimento explícito, mantendo o fluxo auditável.
  • Trade-off: O frontend precisa tratar um terceiro estado de resposta de login (além de sucesso/falha), aumentando levemente a complexidade da tela.
  • < Backward: ASR-007, RF-020
  • > Forward: TASK-059, TASK-060

ADR-012 — Ciclo de Vida do Item como Entidade de Domínio Rica

  • Decisão: A entidade Item (camada domain) encapsula suas transições de estado como métodos (complete(), archive(), delete()), lançando exceções de domínio em transições inválidas (ex.: arquivar item não concluído). Use cases da camada application apenas orquestram: carregam a entidade, chamam o método, persistem via repositório.
  • Por quê: Evita que a regra “arquivamento só após conclusão e só manual” seja reimplementada/esquecida em múltiplos controllers, seguindo o princípio DDD de manter invariantes dentro da entidade.
  • Trade-off: Exige disciplina para não vazar lógica de transição para controllers/DTOs; ligeiramente mais código que um UPDATE status = X direto.
  • < Backward: ASR-012, RF-010
  • > Forward: TASK-025

ADR-013 — Recorrência Calculada sob Demanda com rrule

  • Decisão: Usar a biblioteca rrule para calcular ocorrências de itens recorrentes em tempo de leitura, a partir de frequency, startDate e endDate opcional persistidos no item — sem materializar instâncias em tabela própria.
  • Por quê: rrule já implementa corretamente as regras de recorrência diária/semanal/mensal; a ausência de exceções/overrides (RNF-001) elimina a necessidade de uma tabela de instâncias.
  • Trade-off: Toda consulta que exibe ocorrências (Calendário) paga o custo de expansão em tempo de leitura, em vez de um SELECT simples; aceitável no volume esperado do MVP.
  • < Backward: ASR-011, RF-007, RNF-001
  • > Forward: TASK-027, TASK-034, TASK-036

ADR-014 — Filtro de Snooze Centralizado no Repositório

  • Decisão: O campo snoozedUntil é filtrado (snoozedUntil IS NULL OR snoozedUntil <= now()) em um único método de repositório compartilhado (findVisibleItems), reutilizado por Todo List, Planner Diário e Busca.
  • Por quê: Evita que um módulo esqueça de aplicar o filtro e mostre um item que deveria estar oculto, já que a regra é idêntica em todos os consumidores.
  • Trade-off: Acopla os três módulos a um método de repositório comum; mitigado por ser regra estável e transversal.
  • < Backward: ASR-013, RF-009
  • > Forward: TASK-030, TASK-038, TASK-041, TASK-047

ADR-015 — Regras de Visibilidade de Calendário/Planner como Domain Service

  • Decisão: Implementar as regras de exibição cruzada (SCHEDULE sempre visível; TODO no calendário só com horário/due date; TODO sem data só no planner) em um CalendarVisibilityService na camada domain/application, consumido pelos endpoints de calendário e planner. O frontend apenas renderiza o que a API já retorna filtrado.
  • Por quê: Evita divergência entre o que o backend retorna e o que o frontend decide mostrar, risco real caso a regra fosse replicada em componentes React.
  • Trade-off: Menos flexibilidade para o frontend aplicar filtros visuais adicionais sem nova chamada à API; aceitável pois a regra é normativa (RF-014), não preferência de UI.
  • < Backward: ASR-014, RF-014
  • > Forward: TASK-034, TASK-036, TASK-037, TASK-038, TASK-039

ADR-016 — Busca Textual com ILIKE + Índice Trigram (PostgreSQL)

  • Decisão: Implementar a busca (RF-013) usando ILIKE '%termo%' sobre title da tabela items (filtrado por type = TODO), com índice GIN trigram (pg_trgm) criado via migration Prisma — sem motor de busca dedicado.
  • Por quê: Atende à “busca simples por título” com performance aceitável no volume esperado do MVP, sem a complexidade operacional de manter Elasticsearch/Meilisearch em projeto de um único desenvolvedor.
  • Trade-off: Não suporta busca fonética, fuzzy avançada ou relevância ranqueada; suficiente para a correspondência textual simples exigida pela especificação.
  • < Backward: ASR-015, RF-013
  • > Forward: TASK-041, TASK-042

ADR-017 — Estratégia de Testes: Pirâmide com Jest + SWC, Testcontainers e Supertest

  • Decisão: (1) Testes unitários de domain/application com Jest + SWC, usando repositórios mockados, sem banco real; (2) testes de integração da camada infrastructure (repositórios Prisma) contra PostgreSQL real via Testcontainers; (3) testes e2e de API com Supertest, também sobre Testcontainers, cobrindo os fluxos críticos de autenticação e regras transversais (RNF-002).
  • Por quê: SWC acelera o ciclo de feedback dos testes unitários; Testcontainers evita “funciona no mock mas quebra no Postgres real” para queries com regras específicas do banco (trigram, constraints de FK).
  • Trade-off: Suíte de integração/e2e é mais lenta que testes unitários puros e exige Docker no CI; aceitável frente ao ganho de confiabilidade em regras críticas (cascata de deleção, unicidade item-caderno).
  • < Backward: Especificação (todos os RF/RNF), ASR-001 a ASR-015
  • > Forward: TC-001 a TC-070 (execução automatizada correspondente)

ADR-018 — Validação de Formulário com React Hook Form + Schema Compartilhado

  • Decisão: Formulários no Next.js usam react-hook-form com resolver de schema (Zod), espelhando, quando possível, as regras do backend (ex.: política mínima de senha de RF-017, obrigatoriedade de datas de SCHEDULE de RF-005) — o schema do frontend é tratado como camada de UX, nunca como fonte única de verdade da regra.
  • Por quê: Reduz fricção de UX validando antes do submit, mantendo a validação de domínio no backend como autoridade final — coerente com não confiar em validação client-side para regras de negócio/segurança.
  • Trade-off: Alguma duplicação de regra entre frontend e backend, mitigada por schema compartilhado no monorepo quando o formato permitir.
  • < Backward: RF-017, RF-005, RF-006, ASR-004
  • > Forward: TASK-002, TASK-011, TASK-013, TASK-051, TASK-062, TASK-065

ADR-019 — Storybook para Componentes de UI Isolados dos Módulos de Domínio

  • Decisão: Componentes de apresentação puros (calendário, cards de item, formulários) são desenvolvidos e documentados isoladamente no Storybook, recebendo dados via props — nenhum componente do Storybook chama a API diretamente; a integração com dados reais fica em componentes de página do Next.js.
  • Por quê: Permite construir e validar visualmente as regras complexas de exibição (RF-014) com dados de exemplo controlados, sem depender do backend estar no ar.
  • Trade-off: Exige manter stories/mocks atualizados conforme os componentes evoluem — custo aceito pelo ganho de desenvolvimento visual isolado e documentação viva.
  • < Backward: RF-014, RF-011 (Especificação, seção 2.12)
  • > Forward: Container: Frontend Web (Next.js) — C4 Nível 2

ADR-020 — Estado “Inbox” Representado Implicitamente por notebookId Nulo

  • Decisão: Não existe valor INBOX no enum status do Item. Um item está na Inbox se, e somente se, notebookId IS NULL.
  • Por quê: Evita duas colunas controlando a mesma informação (estado “não classificado”); mantém uma única fonte de verdade, alinhada à decisão já registrada em ASR-009 (“coluna notebook_id nullable — nulo = Inbox”).
  • Trade-off: Consultas que precisem listar “itens ativos e já classificados” compõem duas condições (status = ATIVO AND notebookId IS NOT NULL) em vez de checar um único enum; mitigado centralizando a query em repositório compartilhado, no mesmo padrão do findVisibleItems de ADR-014.
  • < Backward: RF-002, RF-004, ASR-009
  • > Forward: TASK-004, TASK-007, TASK-009

ADR-021 — Deleção de Item como Exclusão Física (Hard Delete)

  • Decisão: Não há valor DELETADO no enum status, nem coluna deletedAt (soft delete). A deleção remove fisicamente a linha via DELETE, dentro de transação (reaproveitando ADR-010), incluindo cascata para SubItem.
  • Por quê: A Especificação (seção 2.8) e UC-007 descrevem a deleção como “remoção total das referências”, não como arquivamento lógico; não há requisito de auditoria ou retenção histórica de itens deletados nesta versão do sistema.
  • Trade-off: Perda irreversível de dados ao deletar (sem “lixeira”/undo); aceito pois nenhum RF exige recuperação pós-deleção no MVP. Uma futura exigência de recuperação implicaria migração para soft delete (deletedAt), registrada aqui como candidata a dívida técnica futura.
  • < Backward: RF-011, G-008, UC-007, ASR-006
  • > Forward: TASK-021, TASK-023, TASK-069

ADR-022 — Auto-relacionamento Unidirecional no Item via scheduleId

  • Decisão: A associação opcional entre um TODO e um SCHEDULE (G-003.3, RF-006) é modelada como FK nullable scheduleId na própria tabela items (self-join), aproveitando a Single Table Inheritance de ADR-004, em vez de uma tabela de associação N:N.
  • Por quê: Como SCHEDULE e TODO residem na mesma tabela, o relacionamento é naturalmente um auto-relacionamento; uma tabela de junção separada seria over-engineering para uma cardinalidade 0..1 unidirecional.
  • Trade-off: A regra “scheduleId só pode apontar para um item do tipo SCHEDULE, e só um TODO pode preenchê-lo” não é garantida por constraint simples de FK (o banco não valida o discriminador type do alvo) — precisa ser validada na entidade de domínio Item, não pelo schema.
  • < Backward: RF-006, G-003.3, ASR-010, ADR-004
  • > Forward: TASK-014

ADR-023 — Proprietário Direto (userId) no Item, Independente do Caderno

  • Decisão: A tabela items possui uma FK obrigatória userId, além da FK opcional notebookId. O vínculo com o Usuário não é inferido apenas transitivamente via Caderno.
  • Por quê: Itens na Inbox (notebookId IS NULL) ainda precisam ser isolados por usuário — RNF-002 exige sessão válida e isolamento de dados para toda operação, inclusive sobre itens não classificados. Sem FK direta, seria impossível checar a propriedade de um item órfão de caderno no guard global (ADR-008).
  • Trade-off: Referência aparentemente duplicada ao usuário quando o item já está classificado (userId direto e, indiretamente, notebookId → userId); mitigado validando na camada de aplicação que ambos sempre apontam para o mesmo usuário, tanto na criação quanto na classificação do item (TASK-007).
  • < Backward: RNF-002, ASR-004, ASR-009
  • > Forward: TASK-004, TASK-007

ADR-024 — “Concluído” Modelado como Flag Booleano, Não como Valor do Enum status

  • Decisão: O enum status do Item possui apenas dois valores: ATIVO e ARQUIVADO. A conclusão de um TODO é representada pelo campo booleano independente isCompleted (já fixado como coluna própria em ADR-004). Um item “Concluído” (RF-010) é, na prática, status = ATIVO AND isCompleted = true; o status só migra para ARQUIVADO na ação manual de arquivamento (G-007.2), que exige isCompleted = true como pré-condição de domínio.
  • Por quê: Evita um terceiro valor de enum (CONCLUIDO) redundante com isCompleted; reduz o espaço de estados inválidos (ex.: impossibilita status = CONCLUIDO em um item SCHEDULE, que sequer possui isCompleted, per ADR-004).
  • Trade-off: A transição “Ativo → Concluído” deixa de ser uma mudança de status e passa a exigir validação de dois campos combinados na entidade de domínio rica (Item.archive() deve checar isCompleted antes de permitir status = ARQUIVADO) — mitigado por já existir essa centralização em ADR-012.
  • < Backward: RF-010, G-007, G-007.1, G-007.2, ADR-004, ADR-012
  • > Forward: TASK-025, TC-025, TC-026

ADR-025 — Confirmação de Deleção de TODO com Sub-itens via 409 + Reenvio

  • Decisão: DELETE /items/{id} retorna 409 Conflict com subItemsCount quando o item possui sub-itens e a query confirm=true não foi enviada. O cliente reenvia a mesma requisição com confirm=true para efetivar a cascata.
  • Por quê: Mantém a semântica REST do verbo DELETE (idempotente, sem corpo de confirmação) e evita um endpoint paralelo só para checar dependências antes de deletar (TASK-023).
  • Trade-off: O frontend precisa tratar 409 como um estado intermediário de UI (exibir modal), não como erro definitivo — documentado explicitamente para não ser confundido com falha genérica.
  • < Backward: RF-011, UC-007, TASK-023
  • > Forward: DELETE /items/{id}, TASK-024

ADR-026 — Reordenação Manual como Operação em Lote

  • Decisão: PATCH /items/reorder recebe o array completo orderedItemIds do escopo afetado (ex.: todos os itens de um caderno), em vez de um campo de posição individual por item.
  • Por quê: Drag & drop reordena a lista inteira visualmente; enviar apenas a nova posição de um item exigiria recalcular a posição de todos os demais no backend de forma implícita e propensa a condição de corrida entre múltiplos usuários/abas.
  • Trade-off: Payload maior para listas extensas; aceitável no volume esperado do MVP (equipe de 1 dev, RISCO documentado em 03-Riscos-Dívida-Tecnica.md caso volumetria cresça).
  • < Backward: RF-012, UC-014, TASK-045, TASK-046
  • > Forward: PATCH /items/reorder, TASK-079

ADR-027 — Sinalização de Sessão Corrente (isCurrent) na Listagem de Sessões

  • Decisão: GET /auth/sessions retorna o campo booleano isCurrent em cada sessão, calculado no backend a partir do cookie da própria requisição.
  • Por quê: Sem esse campo, o frontend precisaria decodificar o JWT no cliente (que não deveria ser acessível via JavaScript, por ser httpOnly — ver 01-Contexto-Container.md, seção 4.2) para saber qual sessão é a corrente antes de oferecer “revogar todas as outras”.
  • Trade-off: Nenhum trade-off relevante — o cálculo é barato (comparação de ID no backend).
  • < Backward: RF-021, UC-017, TASK-055
  • > Forward: GET /auth/sessions, DELETE /auth/sessions, TASK-056

ADR-028 — Renovação de Access Token via Endpoint Dedicado

  • Decisão: Criar POST /auth/refresh, ausente na lista original de Tasks (TASK-001 a TASK-070), como rota dedicada que consome o Refresh Token do cookie httpOnly e emite um novo Access Token.
  • Por quê: O modelo híbrido de dois tokens (RF-021) só funciona de ponta a ponta se existir uma rota de renovação silenciosa; sem ela, o usuário seria deslogado a cada 15 minutos (duração do Access Token).
  • Trade-off: Endpoint adicional não coberto pelas Tasks originalmente levantadas — proposto aqui como lacuna do levantamento inicial (TASK-071, a ser incorporada em 07-Tasks.md).
  • < Backward: RF-021, ASR-003, ADR-006 (Arquitetura)
  • > Forward: POST /auth/refresh, TASK-071

ADR-029 — Reaproveitamento do Código HTTP 429 para Bloqueio de Conta

  • Decisão: POST /auth/login retorna 429 Too Many Requests tanto para o bloqueio de conta por força bruta (RF-019, 5 tentativas) quanto para o throttling por IP (ADR-009, Arquitetura), em vez de usar 423 Locked (semanticamente mais preciso para bloqueio de recurso).
  • Por quê: Mantém consistência com o caso de teste já documentado (TC-053), que fixa 429 como resposta esperada na 6ª tentativa; alterar para 423 exigiria também reabrir e revisar TC-053.
  • Trade-off: Perda de precisão semântica (429 é mais associado a rate limiting de infraestrutura do que a bloqueio de conta específico) — aceito para não gerar dessincronia entre este contrato e o caso de teste já fixado.
  • < Backward: RF-019, ADR-009 (Arquitetura), TC-053
  • > Forward: POST /auth/login, TASK-053, TASK-054