Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ADR 0008 — Contrato HTTP de erro e boundary de observabilidade

## Status
Aceita

## Contexto

A Phase 5 precisa tornar a API previsível para consumidores e, ao mesmo tempo, elevar a rastreabilidade operacional sem introduzir novos módulos de negócio nem mensageria externa. A base já possuía validação por DTO, logger estruturado, contexto de correlação e bootstrap mínimo de OpenTelemetry, mas faltava um contrato público único para erros, métricas mínimas e instrumentação consistente dos fluxos críticos.

## Decisão

Adotar as seguintes regras no boundary da aplicação:

- todo erro HTTP não bem-sucedido retorna `{ error, message, correlation_id }`
- `correlation_id` no payload deve coincidir com o header `x-correlation-id`
- validações de request passam por uma `exceptionFactory` central e retornam `invalid_request`
- falhas técnicas retornam apenas `internal_error` com mensagem genérica
- `GET /metrics` expõe métricas Prometheus sem autenticação na aplicação
- spans manuais cobrem HTTP, banco, bus interno, login, RBAC, autorização e audit logs
- logs do boundary são estruturados, module-aware e sanitizam segredos, cookies e stack traces

## Alternativas consideradas

- manter múltiplos formatos de erro por origem (`HttpException`, `NexusError`, validação)
- introduzir coletor externo de métricas ou tracing já nesta fase
- proteger `GET /metrics` com autenticação da própria aplicação

## Consequências

Positivas:
- contrato de erro fica estável, simples e fácil de testar
- investigações operacionais passam a correlacionar request, log, trace, métrica e audit row
- a aplicação ganha sinais mínimos de engenharia sem elevar muito a complexidade

Negativas:
- o contrato antigo de erro deixa de ser compatível imediatamente
- `GET /metrics` exige disciplina operacional na borda da infraestrutura
- a telemetria continua básica e local, sem exporter distribuído definido nesta fase

## Regras

- não vazar senha, token, `Authorization`, cookies nem stack interna no boundary HTTP
- manter autorização deny-by-default e tenant context explícito em rotas protegidas
- qualquer mudança futura no contrato público de erro deve passar por ADR nova
- qualquer evolução para exporter externo, sampling avançado ou storage dedicado de métricas/traces deve ser reavaliada em ADR futura
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## [2026-03-26]
- feature: implementação da Phase 5 — Quality, Observability & Engineering com contrato de erro padronizado, `GET /metrics`, spans adicionais e logs estruturados/sanitizados.
- feature: paginação pública adicionada a `GET /audit-logs` e `GET /organizations/:id/memberships`, com novos índices compostos na migration `0005_quality_observability_engineering.sql`.
- test: cobertura unitária, de integração e funcional ampliada para validação, `correlation_id`, métricas, autorização deny-by-default e paginação tenant-aware.
- docs: README, comandos, arquitetura, status de fase, handoff e ADR 0008 atualizados para refletir a Phase 5.

## [2026-03-26]
- feature: implementação da Phase 4 — Auditability com módulo `audit-logs`, endpoint `GET /audit-logs` e migration `0004_audit_logs.sql`.
- feature: bus interno síncrono e `RequestCorrelationContext` adicionados para desacoplar o append de auditoria e persistir `correlation_id`.
Expand Down
159 changes: 108 additions & 51 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Nexus Platform

Multi-tenant backend platform for identity, organizations, users, tenant-scoped RBAC and immutable auditability. The codebase remains a modular monolith with DDD, Clean Architecture and explicit internal boundaries.
Multi-tenant backend platform for identity, organizations, users, tenant-scoped RBAC, immutable auditability and operational observability. The codebase remains a modular monolith with DDD, Clean Architecture and explicit internal boundaries.

## 🚧 Project Status

Current Phase: **Phase 4Auditability**
Current Phase: **Phase 5Quality, Observability & Engineering**

Notion reference: [Nexus Platform](https://www.notion.so/mrgomides/Nexus-Platform-32fe01f2262680cd9e32db2b5cdd8f7b?source=copy_link)

Expand All @@ -22,32 +22,35 @@ Notion reference: [Nexus Platform](https://www.notion.so/mrgomides/Nexus-Platfor
- Docker
- GitHub Actions

## What Phase 4 Delivers
## What Phase 5 Delivers

- append-only `audit_logs` storage with immutable PostgreSQL protection against `UPDATE` and `DELETE`
- minimal in-process internal event bus so audited side effects stay decoupled from the main modules
- audit coverage for login, logout, organization lifecycle, membership assignment, RBAC changes and authorization denials
- protected `GET /audit-logs` endpoint with tenant scoping and `audit:view` enforcement
- correlation id propagation from the HTTP request boundary into persisted audit rows
- unit, integration and functional coverage for audit domain rules, event persistence and tenant-scoped queries
- production-oriented validation at the HTTP boundary with DTOs, `ValidationPipe` normalization and a stable error contract
- a single API error format with semantic snake_case codes and `correlation_id` mirrored in `x-correlation-id`
- structured logs with `timestamp`, `level`, `message`, `module`, `correlation_id`, `tenant_id` and `user_id`
- request-level correlation propagation across HTTP, use cases, database access, internal events and audit rows
- manual telemetry instrumentation for HTTP entrypoints, critical use cases, `DatabaseExecutor`, `InternalEventBus` and authorization flow
- scrape-friendly Prometheus metrics at `GET /metrics`
- explicit pagination and reasonable limits for `GET /audit-logs` and `GET /organizations/:id/memberships`
- broader unit, integration and functional coverage for identity, organizations, users, access-control, audit logs, validation and observability

## Available Endpoints

- `GET /health`
- `GET /metrics`
- `POST /identity/accounts`
- `POST /identity/login`
- `POST /identity/logout`
- `POST /organizations`
- `GET /organizations/:id`
- `PATCH /organizations/:id/inactive`
- `POST /organizations/:id/memberships`
- `GET /organizations/:id/memberships`
- `GET /organizations/:id/memberships?limit=<1-100>&offset=<0-1000>`
- `POST /roles`
- `GET /roles`
- `POST /roles/:id/permissions`
- `GET /permissions`
- `POST /users/:id/roles`
- `GET /audit-logs`
- `GET /audit-logs?tenantId=<tenant>&limit=<1-100>&offset=<0-1000>`

## Authorization Model

Expand All @@ -64,19 +67,56 @@ Authenticated User
### Default bootstrap and backfill

- Every tenant receives a default `organization_admin` role.
- The role is granted the full default permission catalog for this phase.
- The role is granted the full default permission catalog for the current phase.
- Existing active memberships were backfilled with `organization_admin` in the Phase 3 migration.
- New organizations bootstrap the creator membership and the `organization_admin` assignment in the same transaction flow.

## Auditability Model
### Default permission catalog

- `organization:view`
- `organization:deactivate`
- `membership:create`
- `membership:view`
- `role:create`
- `role:view`
- `permission:view`
- `role:grant-permission`
- `role:assign`
- `user:create`
- `user:update`
- `audit:view`

## Error Contract

All non-success HTTP responses now follow the same payload shape:

```json
{
"error": "permission_denied",
"message": "Permission denied",
"correlation_id": "2d2dbbc8-b6dc-4c8f-a76e-b8578dd8d6d8"
}
```

Rules applied in Phase 5:

- semantic codes are stable and snake_case
- request validation fails early with `invalid_request`
- functional errors stay explicit
- technical failures return generic `internal_error`
- `x-correlation-id` is always returned and matches `correlation_id` in error payloads
- authentication and logging never expose password, token or stack details at the HTTP boundary

## Auditability And Querying

```text
HTTP request
-> correlation id resolved at the request boundary
-> guards resolve principal + tenant + permission
-> module executes the main use case
-> audited modules publish an internal event in-band
-> audit-logs subscriber appends an immutable row
-> GET /audit-logs reads by tenant with RBAC protection
-> GET /audit-logs reads by tenant with RBAC protection and pagination
```

### Audited actions
Expand All @@ -93,42 +133,55 @@ HTTP request
- `role_assigned`
- `authorization_denied`

### Querying audit logs
### Audit log rules

- `GET /audit-logs?tenantId=<organization-id>`
- optional filters: `userId`, `action`, `from`, `to`
- response fields: `id`, `timestamp`, `userId`, `tenantId`, `action`, `resource`, `metadata`, `correlationId`
- `GET /audit-logs` requires `audit:view`
- `tenantId` in the query must match the active tenant from the authenticated session
- optional filters: `userId`, `action`, `from`, `to`
- pagination defaults to `limit=50` and `offset=0`
- public limits are `limit <= 100` and `offset <= 1000`
- ordering stays `timestamp DESC, id DESC`
- bootstrap-session or failed-login rows may persist `tenantId = null` when no tenant context exists

### Default permission catalog
## Observability

- `organization:view`
- `organization:deactivate`
- `membership:create`
- `membership:view`
- `role:create`
- `role:view`
- `permission:view`
- `role:grant-permission`
- `role:assign`
- `user:create`
- `user:update`
- `audit:view`
### Structured logs

Every HTTP request and main operational event emits structured logs aligned around:

- `timestamp`
- `level`
- `message`
- `module`
- `correlation_id`
- `tenant_id`
- `user_id`

Sensitive headers and values such as `Authorization`, cookies, tokens, passwords and error stacks are sanitized at the boundary.

### Correlation and tracing

## Tenant and Authorization Notes
- incoming requests reuse `x-correlation-id` when present or generate a new UUID
- the correlation id flows into guards, use cases, internal events and persisted audit rows
- manual OpenTelemetry spans cover HTTP entrypoints, critical use cases, `DatabaseExecutor`, `InternalEventBus` and authorization decisions

- `POST /identity/login` still accepts `organizationId` for tenant-bound sessions.
- Users with active memberships must log in with `organizationId`.
- Users without active memberships can still obtain a bootstrap session with `organizationId = null`.
- `POST /organizations` remains authentication-only because it happens before tenant RBAC exists.
- Tenant-scoped organization routes require:
- authenticated principal
- tenant context resolved from the active session
- active organization
- active membership
- matching RBAC permission for the requested action
- Session-scoped RBAC routes (`/roles`, `/permissions`, `/users/:id/roles`) resolve the tenant directly from the active session and still validate organization + membership before authorization.
### Metrics

`GET /metrics` exposes Prometheus-compatible metrics including:

- `nexus_http_requests_total`
- `nexus_http_request_duration_ms`
- `nexus_module_failures_total`
- `nexus_identity_logins_total`
- `nexus_authorization_decisions_total`
- `nexus_audit_operations_total`
- `nexus_audit_operation_duration_ms`

## Testing Strategy

- Unit: domain rules, use cases, error mapping, validation mapping, logging and telemetry helpers
- Integration: real PostgreSQL flows through Testcontainers for login, tenant resolution, authorization, audit immutability, pagination and indexes
- Functional: end-to-end HTTP scenarios for authenticated tenant access, permission denial, audit correlation, metrics exposure and validation failures

## Project Structure

Expand Down Expand Up @@ -198,21 +251,24 @@ NODE_ENV=development
cp .env.example .env
npm install
npm run db:migrate
npm run start:dev
npm run dev
```

The application also applies pending SQL migrations during bootstrap, so `npm run db:migrate` is the explicit operational option and app startup is the safety net.
The application also applies pending SQL migrations during bootstrap, so `npm run db:migrate` is the explicit operational command and app startup remains the safety net.

## Run with Docker
## Run With Docker

```bash
cp .env.example .env
docker compose up -d --build
npm run docker:up
curl http://localhost:3000/health
docker compose logs -f app postgres
docker compose down -v
curl http://localhost:3000/metrics
npm run docker:logs
npm run docker:down
```

The existing `Makefile` mirrors these flows with `make up`, `make down`, `make run`, `make test`, `make test-unit`, `make test-integration` and `make lint`, but the package scripts remain the primary operational commands.

## Test

```bash
Expand All @@ -221,6 +277,7 @@ npm run build
npm run test:unit
npm run test:integration
npm run test:functional
npm run ci
```

`integration` and `functional` suites use Testcontainers and require a running Docker daemon. When Docker is unavailable, those suites are skipped locally; CI runs them with Docker enabled.
Expand All @@ -230,12 +287,12 @@ npm run test:functional
- Modular Monolith remains the deployment model.
- `identity` owns authentication, sessions and the authenticated principal.
- `organizations` owns tenant lifecycle and tenant-scoped membership flows.
- `users` owns the global user record and `memberships`.
- `users` owns the global user record plus `memberships`.
- `access-control` owns roles, permissions, user-role assignments and the final authorization decision.
- `audit-logs` owns the append-only audit trail plus tenant-scoped query access.
- internal events are synchronous and in-process; they exist to decouple audit persistence, not to hide primary business flow.
- PostgreSQL access stays explicit through repositories and SQL, without ORM.
- append-only audit rows are now enforced in storage and correlated to the originating request when present.
- audit and membership queries are tenant-aware, paginated and backed by explicit composite indexes.

## Documentation

Expand Down
Loading
Loading