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) einterface(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 camadadomain. Nenhuma camadadomain/applicationimporta@prisma/clientdiretamente. - 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
itemscom coluna discriminadoratype(SCHEDULE|TODO) e campos específicos por tipo como colunas nulináveis (ex.:startAt/endAtobrigatórios só para SCHEDULE;dueDate,isCompletedsó 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
argon2para 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/passportcompassport-jwtpara 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 tabelasessions. - 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
AuthGuardcomo 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/lockedUntilna tabela de usuário via Prisma; complementarmente,@nestjs/throttlerlimita 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.$transactioncomonDelete: Cascadeno schema para Cadernos/Itens/Sub-itens; exclusão de Caderno usaprisma.$transactioncom 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/loginretornastatus: "requires_reactivation"(HTTP 403) quando a conta estáDesativadae 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(camadadomain) 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 camadaapplicationapenas 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 = Xdireto. <Backward: ASR-012, RF-010>Forward: TASK-025
ADR-013 — Recorrência Calculada sob Demanda com rrule¶
- Decisão: Usar a biblioteca
rrulepara calcular ocorrências de itens recorrentes em tempo de leitura, a partir defrequency,startDateeendDateopcional persistidos no item — sem materializar instâncias em tabela própria. - Por quê:
rrulejá 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
SELECTsimples; 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
CalendarVisibilityServicena 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%'sobretitleda tabelaitems(filtrado portype = 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-formcom 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
INBOXno enumstatusdoItem. 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_idnullable — 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 dofindVisibleItemsde 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
DELETADOno enumstatus, nem colunadeletedAt(soft delete). A deleção remove fisicamente a linha viaDELETE, dentro de transação (reaproveitando ADR-010), incluindo cascata paraSubItem. - 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
scheduleIdna própria tabelaitems(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 “
scheduleIdsó 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 discriminadortypedo alvo) — precisa ser validada na entidade de domínioItem, 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
itemspossui uma FK obrigatóriauserId, além da FK opcionalnotebookId. 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 (
userIddireto 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
statusdo Item possui apenas dois valores:ATIVOeARQUIVADO. A conclusão de um TODO é representada pelo campo booleano independenteisCompleted(já fixado como coluna própria em ADR-004). Um item “Concluído” (RF-010) é, na prática,status = ATIVO AND isCompleted = true; ostatussó migra paraARQUIVADOna ação manual de arquivamento (G-007.2), que exigeisCompleted = truecomo pré-condição de domínio. - Por quê: Evita um terceiro valor de enum (
CONCLUIDO) redundante comisCompleted; reduz o espaço de estados inválidos (ex.: impossibilitastatus = CONCLUIDOem um item SCHEDULE, que sequer possuiisCompleted, per ADR-004). - Trade-off: A transição “Ativo → Concluído” deixa de ser uma mudança de
statuse passa a exigir validação de dois campos combinados na entidade de domínio rica (Item.archive()deve checarisCompletedantes de permitirstatus = 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}retorna409 ConflictcomsubItemsCountquando o item possui sub-itens e a queryconfirm=truenão foi enviada. O cliente reenvia a mesma requisição comconfirm=truepara 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
409como 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/reorderrecebe o array completoorderedItemIdsdo 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.mdcaso 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/sessionsretorna o campo booleanoisCurrentem 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— ver01-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 cookiehttpOnlye 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/loginretorna429 Too Many Requeststanto 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 usar423 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
429como resposta esperada na 6ª tentativa; alterar para423exigiria 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