Browse Source

documentação backend

leonardo 2 tháng trước cách đây
mục cha
commit
d3288dbdc3
5 tập tin đã thay đổi với 638 bổ sung0 xóa
  1. 126 0
      docs/api.md
  2. 135 0
      docs/architecture.md
  3. 107 0
      docs/auth.md
  4. 149 0
      docs/conventions.md
  5. 121 0
      docs/setup.md

+ 126 - 0
docs/api.md

@@ -0,0 +1,126 @@
+# Referência de API
+
+Base URL: `http://localhost:3001`
+
+**Níveis de auth:**
+- **Pública** — sem token
+- **Usuário** — requer JWT válido (qualquer nível)
+- **Admin** — requer JWT com `nivel = "3"`
+
+> **Formato de resposta:** os domínios **Auth** e **Usuários** envolvem a resposta em um wrapper `{ status, msg, ... }`. Os demais retornam o objeto direto (ex: `{ items }`, `{ ok: true }`).
+
+---
+
+## Health
+
+| Método | Rota | Auth | Descrição |
+|---|---|---|---|
+| GET | `/health` | Pública | Status dos serviços (API, banco, Qdrant, Ollama) |
+
+**Resposta:**
+```json
+{ "ok": true, "checks": { "api": true, "db": true, "qdrant": true, "ollama": true } }
+```
+
+---
+
+## Auth (`/api/auth`)
+
+| Método | Rota | Auth | Body | Retorno |
+|---|---|---|---|---|
+| POST | `/api/auth/login` | Pública | `{ login, senha }` | `200 { status, msg, usuario, accessToken?, refreshToken? }` |
+| POST | `/api/auth/logout` | Pública | `{ refreshToken }` | `200 { status, msg }` |
+| POST | `/api/auth/refresh` | Pública | `{ refreshToken }` | `200 { status, accessToken }` |
+
+- `login` aceita e-mail ou login; os campos também são aceitos em PascalCase (`Login`/`Senha`).
+- `accessToken`/`refreshToken` só são emitidos quando `JWT_SECRET` está configurado.
+- O login pode retornar `429` após 5 tentativas incorretas (bloqueio de 15 minutos).
+
+---
+
+## Chat (`/api/chat`)
+
+| Método | Rota | Auth | Body | Retorno |
+|---|---|---|---|---|
+| POST | `/api/chat` | Usuário | `{ message, conversationId?, sessionId?, model?, options? }` | `{ answer, sources }` |
+| POST | `/api/chat/stream` | Usuário | `{ message, conversationId? }` | Stream SSE |
+
+- O campo de texto é **`message`** (não `content`).
+- `options` aceita `{ temperature?, top_p? }`.
+- **Stream SSE:** cada linha tem o formato `data: { "type": "delta" | "sources" | "error", ... }`, terminada por `data: [DONE]`.
+
+---
+
+## Conversas (`/api/conversations`)
+
+| Método | Rota | Auth | Body / Params | Retorno |
+|---|---|---|---|---|
+| GET | `/api/conversations` | Usuário | `?limit&offset` | `{ items }` |
+| POST | `/api/conversations` | Usuário | `{ title? }` | `{ id, title }` |
+| GET | `/api/conversations/:id/messages` | Usuário | — | `{ items }` ou `404 { error: "not_found" }` |
+| PATCH | `/api/conversations/:id` | Usuário | `{ title }` | `{ ok: true }` |
+| DELETE | `/api/conversations/:id` | Usuário | — | `200 { ok: true }` |
+| GET | `/api/conversations/:id/export` | Usuário | — | `{ markdown }` ou `404` |
+
+- Cada item de `GET /api/conversations` traz `Id`, `Title`, `CreatedAt`, `UpdatedAt`, `MessageCount`.
+
+---
+
+## Documentos (`/api/documents`)
+
+| Método | Rota | Auth | Body / Params | Retorno |
+|---|---|---|---|---|
+| GET | `/api/documents` | Usuário | `?limit&offset` | `{ items, nextOffset }` |
+| DELETE | `/api/documents/source/:source` | Admin | — | `{ ok: true }` |
+
+---
+
+## Ingest (`/api/ingest`)
+
+| Método | Rota | Auth | Body | Retorno |
+|---|---|---|---|---|
+| POST | `/api/ingest` | Admin | `{ documents: [{ text, source?, id?, metadata? }] }` | `{ upserted }` |
+| POST | `/api/ingest/file` | Admin | `multipart/form-data` com campo `file` (+ `source?`), máx 25 MB | `{ upserted, documents, extractedChars }` |
+| POST | `/api/ingest/url` | Admin | `{ url, source? }` | `{ upserted, source, extractedChars }` |
+
+- Em `POST /api/ingest`, o campo raiz é o array **`documents`**, e o texto de cada item é **`text`**.
+- Em `POST /api/ingest/url`, a `url` deve começar com `https://`.
+- **Formatos de arquivo suportados:** TXT, PDF e DOCX (processados via Mammoth/pdf-parse).
+
+---
+
+## Busca (`/api/search`)
+
+| Método | Rota | Auth | Body | Retorno |
+|---|---|---|---|---|
+| POST | `/api/search` | Usuário | `{ query, topK? }` | `{ results }` |
+
+---
+
+## Usuários (`/api/users`)
+
+| Método | Rota | Auth | Body | Retorno |
+|---|---|---|---|---|
+| GET | `/api/users` | Admin | — | `{ status, items }` |
+| POST | `/api/users` | Admin | `{ Nome, Login, Email, Senha, Nivel, Setor }` | `201 { status, usuario }` |
+| PUT | `/api/users/:id` | Admin | `{ Nome?, Email?, Nivel?, Setor? }` | `{ status, usuario }` |
+| PATCH | `/api/users/:id/status` | Admin | — | `{ status, novoStatus }` |
+| PATCH | `/api/users/:id/senha` | Próprio ou Admin | `{ senhaAtual, senhaNova }` | `200 { status, msg }` |
+
+- Campos de body em **PascalCase**. Em `POST`, todos são obrigatórios; `Nivel` aceita `"1"`, `"2"` ou `"3"`.
+- `PUT` atualiza parcialmente e **não** aceita alterar `Login`. Exige ao menos um campo.
+- `PATCH /:id/senha` pode ser chamado pelo próprio usuário ou por um admin. Pode retornar `409` (login já cadastrado) no `POST`.
+
+---
+
+## Erros comuns
+
+| Status | Significado |
+|---|---|
+| `400` | Body/params inválidos (falha de validação zod) |
+| `401` | Token ausente, inválido ou expirado; credenciais incorretas |
+| `403` | Autenticado, mas sem permissão (nível insuficiente) |
+| `404` | Recurso não encontrado |
+| `409` | Conflito (ex: login já cadastrado) |
+| `429` | Excesso de requisições / login bloqueado por tentativas |
+| `500` | Erro interno do servidor |

+ 135 - 0
docs/architecture.md

@@ -0,0 +1,135 @@
+# Arquitetura — Backend
+
+## Visão geral
+
+O backend segue uma arquitetura em camadas com bootstrap por factories. Cada domínio de negócio é encapsulado em um conjunto padronizado de arquivos.
+
+```
+index.js
+└── CoreFactory.Iniciar()
+    ├── BancoFactory   → conecta ao MySQL, valida conexão
+    └── ServerFactory  → cria o Express app, registra middlewares, sobe o servidor
+         └── Roteamento.IniciarRoteamento(app)
+              └── Domínios: Auth · Chat · Conversations · Documents · Ingest · Search · Usuario
+                   └── *.Rotas.js → *.Controller.js → *Service.js → Model (Objection)
+```
+
+## Camadas
+
+### Factories (`src/factories/`)
+
+Responsáveis pelo bootstrap da aplicação. Chamadas em sequência por `index.js` na raiz.
+
+| Factory | Responsabilidade |
+|---|---|
+| `Core.factory.js` | Orquestra Banco + Server |
+| `Banco.factory.js` | Conecta ao MySQL e valida a conexão |
+| `Server.factory.js` | Cria o app Express, registra middlewares globais, sobe o servidor HTTP |
+
+### Roteamento (`src/routes/index.js`)
+
+A classe `Roteamento` registra todos os domínios no Express via `IniciarRoteamento(app)`. Cada domínio tem seu próprio arquivo `*.Rotas.js` que define as rotas e middlewares daquele domínio.
+
+Todos os domínios são montados sob o prefixo `/api`, que recebe **dois middlewares globais** antes de qualquer rota: `rateLimitMiddleware` (`RateLimit.js`) e `authMiddleware` (`Validajwt.js`). Ou seja, o JWT é decodificado globalmente; os middlewares `requireUser`/`requireAdmin` por rota apenas verificam o resultado.
+
+### Controllers (`src/controllers/`)
+
+Recebem a requisição HTTP, extraem os dados necessários (params, body, user) e delegam ao Service correspondente. Não contêm lógica de negócio.
+
+### Services (`src/services/`)
+
+Contêm toda a lógica de negócio. São chamados pelos controllers. Podem chamar Models (Objection) ou clients externos (Qdrant, Ollama).
+
+### Models (`src/models/`)
+
+Modelos Objection.js. Mapeiam as tabelas do MySQL e definem relacionamentos.
+
+| Model | Tabela |
+|---|---|
+| `Usuario.model.js` | `usuarios` |
+| `RefreshToken.model.js` | `refresh_tokens` |
+| `Conversation.model.js` | `conversations` |
+| `Message.model.js` | `messages` |
+
+### Middleware (`src/middleware/`)
+
+| Arquivo | Função |
+|---|---|
+| `Validajwt.js` | Verifica e decodifica o JWT do header Authorization |
+| `RequireUser.js` | Exige usuário autenticado (qualquer nível) |
+| `RequireAdmin.js` | Exige nível "3" (administrador) |
+| `Validate.js` | Valida body/params/query com schema zod (`schema.parse()`) |
+| `ParseId.js` | Converte `req.params.id` para número e valida |
+| `RateLimit.js` | Rate limiting por IP |
+| `ErrorHandler.js` | Captura erros e formata a resposta de erro |
+
+### Schemas de validação (`src/middleware/schemas/`)
+
+Schemas **zod** usados com o middleware `Validate` (que chama `schema.parse()`). Um arquivo por domínio que exige validação:
+
+| Arquivo | Domínio |
+|---|---|
+| `Chat.Schema.js` | Corpo do chat |
+| `Ingest.Schema.js` | Corpo do ingest |
+| `Search.Schema.js` | Corpo da busca |
+| `Usuario.Schema.js` | Criar, atualizar e alterar senha de usuário |
+
+## Padrão de arquivos por domínio
+
+Todo domínio segue a convenção de nomenclatura abaixo:
+
+```
+src/
+├── routes/        NomeDoModulo.Rotas.js
+├── controllers/   NomeDoModulo.Controller.js
+├── services/      nomeDoModuloService.js
+├── models/        NomeDoModulo.model.js          (se houver tabela)
+└── middleware/
+    └── schemas/   NomeDoModulo.Schema.js         (se houver validação)
+```
+
+## Alias ESM (`#*`)
+
+O `package.json` define um alias de importação para evitar caminhos relativos longos:
+
+```json
+"imports": {
+  "#*": "./src/*"
+}
+```
+
+Como ficaria:
+```js
+import { UsuarioController } from "#controllers/Usuario.Controller.js";
+import { db } from "#config/db.config.js";
+```
+
+> **Estado atual:** o alias está definido e funciona, mas **ainda não é usado** em nenhum arquivo do `src/` — todo o código usa imports relativos (`../config/db.config.js`). Foi adicionado na refatoração para espelhar o `@` do `minha_back`, mas a migração dos imports não foi feita.
+
+## Domínios existentes
+
+| Domínio | Prefix da rota | Descrição |
+|---|---|---|
+| Auth | `/api/auth` | Login, logout, refresh de token |
+| Chat | `/api/chat` | Envio de mensagens ao LLM |
+| Conversations | `/api/conversations` | CRUD de conversas persistidas |
+| Documents | `/api/documents` | Listagem e exclusão de documentos |
+| Ingest | `/api/ingest` | Ingestão de conteúdo na base RAG |
+| Search | `/api/search` | Busca semântica nos documentos |
+| Usuario | `/api/users` | CRUD de usuários do sistema |
+
+## Health check
+
+`GET /health` — retorna o status de todos os serviços:
+
+```json
+{
+  "ok": true,
+  "checks": {
+    "api": true,
+    "db": true,
+    "qdrant": true,
+    "ollama": true
+  }
+}
+```

+ 107 - 0
docs/auth.md

@@ -0,0 +1,107 @@
+# Autenticação — Backend
+
+## Visão geral
+
+O backend usa JWT com dois tokens: `accessToken` (curta duração) e `refreshToken` (longa duração). O `AUTH_MODE` no `.env` controla o comportamento:
+
+| `AUTH_MODE` | Comportamento |
+|---|---|
+| `jwt` | Auth JWT ativa (padrão e recomendado) |
+| `none` | Auth desativada — **nunca usar em produção** |
+
+---
+
+## Fluxo completo
+
+```
+1. LOGIN
+   POST /api/auth/login
+   Body: { login, senha }
+   ──────────────────────
+   Resposta: { accessToken, refreshToken, usuario }
+
+   accessToken  → duração curta (padrão: 15 min)
+   refreshToken → duração longa (padrão: 30 dias), salvo na tabela refresh_tokens
+
+2. USO DA API
+   Enviar o accessToken no header de cada requisição:
+   Authorization: Bearer <accessToken>
+
+   O middleware Validajwt decodifica o token e popula req.user.
+
+3. REFRESH (quando accessToken expirar)
+   POST /api/auth/refresh
+   Body: { refreshToken }
+   ──────────────────────
+   Resposta: { accessToken }
+
+   O refreshToken permanece válido até expirar ou ser revogado.
+
+4. LOGOUT
+   POST /api/auth/logout
+   Body: { refreshToken }
+   ──────────────────────
+   Revoga o refreshToken no banco. O accessToken expira naturalmente.
+```
+
+---
+
+## Níveis de acesso
+
+O campo `nivel` no model `Usuario` define o que cada usuário pode fazer:
+
+O campo `Nivel` aceita os valores `"1"`, `"2"` e `"3"` (enum em `Usuario.Schema.js`).
+
+| Valor | Perfil | Permissões |
+|---|---|---|
+| `"1"` | Usuário comum | Chat, busca, leitura de documentos, conversas próprias, alterar própria senha |
+| `"2"` | Intermediário | Nível existente no enum, sem regra dedicada no código atual (tratado como não-admin) |
+| `"3"` | Administrador | Tudo do nível 1 + gerenciar usuários, ingerir documentos, excluir documentos |
+
+> **Observação:** apenas `requireAdmin` (exige `"3"`) e o guard de frontend (que redireciona explicitamente o nível `"1"` de `/usuarios`) tratam níveis de forma diferenciada. O nível `"2"` não tem comportamento próprio hoje.
+
+---
+
+## Middleware de auth
+
+| Middleware | Uso |
+|---|---|
+| `Validajwt` | Decodifica o token e popula `req.user`. Usado antes de `RequireUser`/`RequireAdmin` |
+| `RequireUser` | Exige `req.user` presente (qualquer nível) |
+| `RequireAdmin` | Exige `req.user.nivel === "3"` |
+
+**Como aplicar em novas rotas:**
+
+```js
+// Rota aberta (sem auth)
+router.post("/login", AuthController.Login);
+
+// Rota para qualquer usuário autenticado
+router.get("/dados", requireUser, Controller.Listar);
+
+// Rota exclusiva para admins
+router.delete("/recurso/:id", requireUser, requireAdmin, Controller.Remover);
+```
+
+> `requireAdmin` sempre deve vir **após** `requireUser` na lista de middlewares.
+
+---
+
+## Armazenamento dos tokens
+
+- `accessToken`: nunca persistido no banco — é stateless (validado pela assinatura JWT)
+- `refreshToken`: persistido na tabela `refresh_tokens` com `UserId`, `Token` (hash), `ExpiresAt` e `RevokedAt`
+- Um job de limpeza (`cleanupTokens.js`) remove tokens expirados periodicamente
+
+---
+
+## Configuração do JWT
+
+Variáveis de ambiente relevantes (ver [setup.md](setup.md) para descrição completa):
+
+```
+JWT_SECRET=<chave-secreta>
+JWT_ISSUER=oraculo-api
+JWT_ACCESS_TTL_SECONDS=900       # 15 minutos
+JWT_REFRESH_TTL_SECONDS=2592000  # 30 dias
+```

+ 149 - 0
docs/conventions.md

@@ -0,0 +1,149 @@
+# Convenções — Backend
+
+## Como criar um novo domínio
+
+Siga a sequência abaixo ao adicionar um novo módulo ao backend. O exemplo usa o domínio hipotético `Produto`.
+
+---
+
+### 1. Schema de validação (se houver body a validar)
+
+Crie `src/middleware/schemas/Produto.Schema.js`:
+
+```js
+import { z } from "zod";
+
+export const criarProdutoSchema = z.object({
+  nome: z.string().min(2).max(100),
+  preco: z.number().positive(),
+});
+```
+
+---
+
+### 2. Model Objection (se houver tabela no banco)
+
+Crie `src/models/Produto.model.js`:
+
+```js
+import { Model } from "objection";
+
+export class Produto extends Model {
+  static get tableName() { return "produtos"; }
+}
+```
+
+Crie também a migration correspondente em `db/migrations/` (arquivos `.cjs`; o diretório é configurado no `knexfile.js`).
+
+---
+
+### 3. Service
+
+Crie `src/services/produtoService.js` com toda a lógica de negócio:
+
+```js
+import { Produto } from "#models/Produto.model.js";
+
+export async function listarProdutos() {
+  return Produto.query().orderBy("Nome");
+}
+
+export async function criarProduto(dados) {
+  return Produto.query().insert(dados);
+}
+```
+
+---
+
+### 4. Controller
+
+Crie `src/controllers/Produto.Controller.js`. Controllers são finos — apenas extraem dados da requisição e delegam ao service:
+
+```js
+import * as produtoService from "#services/produtoService.js";
+import { AppError } from "#shared/errors/AppError.js";
+
+export const ProdutoController = {
+  async Listar(req, res, next) {
+    try {
+      const produtos = await produtoService.listarProdutos();
+      res.json({ produtos });
+    } catch (err) {
+      next(err);
+    }
+  },
+
+  async Criar(req, res, next) {
+    try {
+      const produto = await produtoService.criarProduto(req.body);
+      res.status(201).json({ produto });
+    } catch (err) {
+      next(err);
+    }
+  },
+};
+```
+
+---
+
+### 5. Rotas
+
+Crie `src/routes/Produto.Rotas.js`:
+
+```js
+import { Router } from "express";
+import { ProdutoController } from "#controllers/Produto.Controller.js";
+import { requireUser } from "#middleware/RequireUser.js";
+import { requireAdmin } from "#middleware/RequireAdmin.js";
+import { validate } from "#middleware/Validate.js";
+import { criarProdutoSchema } from "#middleware/schemas/Produto.Schema.js";
+
+export const produtosRouter = Router();
+
+produtosRouter.get("/",  requireUser,                                    ProdutoController.Listar);
+produtosRouter.post("/", requireUser, requireAdmin, validate({ body: criarProdutoSchema }), ProdutoController.Criar);
+```
+
+---
+
+### 6. Registro no Roteamento
+
+Adicione o novo router em `src/routes/index.js`:
+
+```js
+import { produtosRouter } from "./Produto.Rotas.js";
+
+// dentro de IniciarRoteamento(app):
+app.use("/api/produtos", produtosRouter);
+```
+
+---
+
+## Regras gerais
+
+**Nomenclatura:**
+- Arquivos de rotas: `NomeDoModulo.Rotas.js` (PascalCase)
+- Arquivos de controller: `NomeDoModulo.Controller.js` (PascalCase)
+- Arquivos de service: `nomeDoModuloService.js` (camelCase)
+- Arquivos de model: `NomeDoModulo.model.js` (PascalCase)
+- Arquivos de schema: `NomeDoModulo.Schema.js` (PascalCase)
+
+**Imports:** o código atual usa caminhos **relativos** em todo o `src/` — siga esse padrão para manter consistência:
+```js
+import { db } from "../config/db.config.js";
+```
+> O `package.json` define o alias `#*` → `./src/*`, que é válido e funciona, mas ainda **não é usado** em nenhum arquivo do `src/`. Por ora, prefira imports relativos para alinhar ao restante do código. (Os exemplos acima usam `#` apenas para ilustrar; o código real usa `../`.)
+
+**Auth nas rotas:**
+- Leitura pública: sem middleware de auth
+- Leitura autenticada: `requireUser`
+- Escrita de dados globais / operações destrutivas: `requireUser, requireAdmin`
+- Consulte [auth.md](auth.md) para entender os níveis de acesso
+
+**Tratamento de erros:**
+- Sempre encapsule o corpo do controller em `try/catch` e passe o erro para `next(err)`
+- Use `AppError` para erros esperados (ex: não encontrado, sem permissão)
+- O `ErrorHandler` global formata a resposta automaticamente
+
+**Documentação:**
+- Ao criar novas rotas, adicione os endpoints em [api.md](api.md)

+ 121 - 0
docs/setup.md

@@ -0,0 +1,121 @@
+# Setup — Backend
+
+## Pré-requisitos
+
+- Node.js 18+ (ver `engines.node` no `package.json`)
+- MySQL 8+ acessível na rede
+- Qdrant rodando (padrão: `http://localhost:6333`)
+- Ollama rodando com os modelos necessários:
+  ```bash
+  ollama pull nomic-embed-text
+  ollama pull llama3.1
+  ```
+
+## Instalação
+
+```bash
+cd backend
+npm install
+```
+
+## Variáveis de ambiente
+
+Crie o arquivo `backend/.env` com base nas variáveis abaixo:
+
+### Servidor
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `PORT` | Porta HTTP da API | `3001` |
+| `CORS_ORIGIN` | Origem permitida pelo CORS | `http://localhost:5173` |
+| `API_KEY` | Chave de API opcional | — |
+| `RATE_LIMIT_PER_MINUTE` | Limite de requisições por minuto (`0` desativa) | `0` |
+
+### Banco de dados (MySQL)
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `DB_HOST` | Host do MySQL | `localhost` |
+| `DB_PORT` | Porta do MySQL | `3306` |
+| `DB_USER` | Usuário do banco | `root` |
+| `DB_PASS` | Senha do banco | `senha` |
+| `DB_SCHEMA` | Nome do schema/database | `oracle` |
+
+### Qdrant (busca vetorial)
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `QDRANT_URL` | URL do Qdrant | `http://localhost:6333` |
+| `QDRANT_COLLECTION` | Nome da collection (default no código: `oraculo_docs`) | `empresa_docs` |
+| `QDRANT_API_KEY` | Chave de API do Qdrant (opcional) | — |
+
+### Ollama (LLM local)
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `OLLAMA_URL` | URL do Ollama | `http://localhost:11434` |
+| `OLLAMA_EMBEDDINGS_MODEL` | Modelo para embeddings | `nomic-embed-text` |
+| `OLLAMA_CHAT_MODEL` | Modelo para chat | `llama3.1` |
+| `OLLAMA_VISION_MODEL` | Modelo de visão (default `llava:latest`) | `llava:latest` |
+
+### RAG
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `RAG_QUERY_REWRITE` | Reescrever query antes da busca | `true` |
+| `RAG_HISTORY_TOKEN_BUDGET` | Tokens de histórico enviados ao LLM | `1500` |
+| `RAG_TOP_K` | Número de chunks recuperados (default `6`) | `8` |
+| `RAG_MIN_SCORE` | Score mínimo de similaridade | `0.45` |
+| `RAG_CHUNK_SIZE` | Tamanho de cada chunk (caracteres) | `900` |
+| `RAG_CHUNK_OVERLAP` | Sobreposição entre chunks | `150` |
+
+### LLM (parâmetros de geração)
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `LLM_TEMPERATURE` | Criatividade das respostas (0–1) | `0.7` |
+| `LLM_TOP_P` | Nucleus sampling | `0.9` |
+| `LLM_NUM_PREDICT` | Máximo de tokens gerados | `2048` |
+| `LLM_REPEAT_PENALTY` | Penalidade de repetição | `1.1` |
+
+### Autenticação (JWT)
+
+| Variável | Descrição | Exemplo |
+|---|---|---|
+| `AUTH_MODE` | Modo de auth (`jwt` ou `none`) | `jwt` |
+| `JWT_SECRET` | Chave secreta para assinar tokens | `trocar-em-producao` |
+| `JWT_ISSUER` | Issuer do token | `oraculo-api` |
+| `JWT_ACCESS_TTL_SECONDS` | Duração do access token em segundos | `900` (15 min) |
+| `JWT_REFRESH_TTL_SECONDS` | Duração do refresh token em segundos | `2592000` (30 dias) |
+
+> **Atenção:** `AUTH_MODE=none` desativa a autenticação completamente. Nunca usar em produção.
+
+## Rodando localmente
+
+```bash
+# Criar as tabelas no banco (rodar uma vez ou após novas migrations)
+npm run migrate:latest
+
+# Modo desenvolvimento (hot reload)
+npm run dev
+
+# Modo produção
+npm start
+```
+
+A API estará disponível em `http://localhost:3001`.
+
+Para verificar o status dos serviços:
+```
+GET http://localhost:3001/health
+```
+
+## Comandos disponíveis
+
+| Comando | Descrição |
+|---|---|
+| `npm run dev` | Inicia com hot reload (`node --watch`) |
+| `npm start` | Inicia sem hot reload |
+| `npm run migrate:latest` | Aplica todas as migrations pendentes |
+| `npm run migrate:rollback` | Reverte a última migration |
+| `npm run seed:run` | Executa os seeds do banco |