# Catcher Data — documentação completa (para LLMs) > Um único arquivo com todo o manual de consumo da API. Fonte navegável: https://data.catcher.one/docs · API: https://data-api.catcher.one · MCP: https://data-api.catcher.one/v1/mcp --- # Getting Started — consumindo o Catcher Data Tudo que você precisa antes de chamar qualquer um dos seis produtos. ## 1. Base URL + formato | Superfície | URL | |---|---| | **API** | **`https://data-api.catcher.one`** | | **Console** (dashboard) | **`https://data.catcher.one`** | Tudo é JSON (`Content-Type: application/json`), versão `v1`. As rotas dos produtos são `/v1/clouddb/*` (Database), `/v1/cloudvec/*` (Vector), `/v1/cloudcache/*` (Cache), `/v1/cloudstore/*` (Storage), `/v1/cloudwiki/*` (Wiki) e `/v1/mcp` (MCP). ## 2. Autenticação (API key) Para acesso programático / server-to-server, use uma **API key** no header: ``` X-API-Key: ctc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` (Há também JWT `Authorization: Bearer ` para sessões de usuário no Console; para integrações use a API key. O MCP usa um token próprio — veja [MCP](mcp.md).) ### Mintar uma API key Keys são criadas por um **owner** (endpoint owner-only, exige email verificado) — pelo Console em **API Keys**, ou por API: ```bash curl -sX POST https://data-api.catcher.one/v1/tokens \ -H "Authorization: Bearer " \ -H 'Content-Type: application/json' \ -d '{ "label": "meu-servico", "role": "owner" }' ``` ```json { "token_id": 12, "token": "ctc_…", "role": "owner", "last4": "ab12", "created_at": "…" } ``` > ⚠️ O valor `token` aparece **uma única vez** — guarde no seu secret manager. > Opcional: `allowed_source_ips` (CSV de IPv4/IPv6/CIDR) prende a key a IPs. > Revogue com `DELETE /v1/tokens/{id}`. A partir daí, todo request leva `-H "X-API-Key: ctc_…"`. ## 3. Papéis `owner` > `admin` > `agent`. **Os produtos (Database, Vector, Cache, Storage, Wiki e a gestão do MCP) exigem `owner` ou `admin`.** Um token `agent` recebe `403 FORBIDDEN` nessas rotas. ## 4. Projetos vêm primeiro Database, Storage, Wiki e as coleções do Vector vivem dentro de um **projeto**. Um projeto = um projeto GCP + billing + estratégia de tenancy. ```bash curl -sX POST https://data-api.catcher.one/v1/clouddb/projects \ -H "X-API-Key: ctc_…" -H 'Content-Type: application/json' \ -d '{ "name": "Produção", "hosting_mode": "catcher", "strategy": "row_level" }' # → { "id": "", ... } guarde o id, ele é o {pid} das rotas ``` - `hosting_mode`: `catcher` (infra + billing da Catcher, custo repassado com margem fixa) ou `byo` (seu próprio GCP/billing). - `strategy`: `row_level` (um schema compartilhado, coluna de tenant) ou `schema_per_company` (um schema por end-customer). Detalhes na página do [Database](database.md). O mesmo `{pid}` é usado pelo Storage (`/v1/cloudstore/projects/{pid}/buckets`), pelas coleções do Vector (`/v1/cloudvec/projects/{pid}/collections`) e pelos espaços da Wiki (`/v1/cloudwiki/projects/{pid}/spaces`). ### Conta → Projeto → Tenant Há **três níveis**: sua **Conta** (de onde sai a API key), os **Projetos** dentro dela, e — se você atende vários clientes finais — os **Tenants** (o cliente do seu cliente). Um mesmo `tenant_id` isola um cliente final nos produtos, cada um com o mecanismo nativo da sua tecnologia. Veja **[Tenants](tenants.md)** antes de modelar multi-tenancy. ## 5. Envelope de erro Toda resposta de erro tem o mesmo formato: ```json { "error_code": "INSTANCE_NOT_READY", "message": "instance is not connected …", "trace_id": "2a62142c-…", "error": "…" } ``` - **`error_code`** — código estável `SCREAMING_SNAKE_CASE`. **Faça o match nisso** (não na `message`). Cada módulo lista os seus na sua página. - **`trace_id`** — também vai no header `X-Trace-ID`. Cole no suporte para localizarmos a linha exata de log do backend. - Validações de campo podem trazer `field_errors: [{field, message}]`. Códigos genéricos por status: `BAD_REQUEST` (400), `UNAUTHORIZED` (401), `FORBIDDEN` (403), `NOT_FOUND` (404), `CONFLICT` (409), `UNPROCESSABLE_ENTITY` (422), `RATE_LIMITED` (429), `SERVICE_UNAVAILABLE` (503). ## 6. Convenções - **Idempotência:** criações que mutam aceitam `Idempotency-Key: `; um retry com a mesma chave devolve o estado atual em vez de duplicar. - **Paginação:** onde existe, é `?page=&limit=` (máx. 100) — a maioria das listas dos produtos é escopada (por projeto/coleção/espaço) e não pagina. - **Feature flags:** se um módulo não está habilitado no ambiente, suas rotas retornam **`404`** (não `403`). Um `404` "fantasma" numa rota correta = módulo desligado; fale com o suporte. - **Escopo por conta:** a API key carrega a sua conta; você só enxerga os recursos dela. Isolamento entre contas é validado por suite de pentest contínua (cross-tenant, auth, RBAC, injeção). - **Assíncrono:** provisionar uma instância de Database, ativar um Cache dedicado e ingerir um arquivo (Vector/Wiki) são operações assíncronas — a resposta é imediata (`pending`/`provisioning`/`processing`) e você faz poll do status. ## Próximo passo Vá para o produto que você vai usar: **[Database](database.md)** · **[Vector](vector.md)** · **[Cache](cache.md)** · **[Storage](storage.md)** · **[Wiki](wiki.md)** · **[MCP](mcp.md)** — e leia **[Tenants](tenants.md)** se o seu app atende múltiplos clientes finais. --- # Database — Cloud SQL gerenciado (`clouddb`) > Rotas `/v1/clouddb/*` · owner/admin · gated por `CLOUDDB_ENABLED` (off → 404) ## O que é Um **banco relacional gerenciado** (PostgreSQL ou MySQL, em cima do Google Cloud SQL) que você provisiona e opera **pela API**, sem tocar no console da GCP. Além do banco em si, o módulo te dá: - um **SQL editor guardado** (roda uma instrução por vez, default read-only), - um **schema declarativo versionado** (descreve o schema uma vez, aplica com diff + confirmação para mudanças destrutivas + rollback), - **multi-tenancy de end-customers** (isola os clientes do SEU cliente por linha ou por schema), e - **billing** (custo GCP + markup). ## O que dá pra fazer - Provisionar instâncias Cloud SQL (`postgres`/`mysql`) por projeto. - Rodar SQL com um guard de segurança (default-deny: sem statements empilhados, sem comentários; write/DDL só com flag explícita) + histórico. - Declarar o schema (IR `catcher.schema.v1`), pré-visualizar o diff, aplicar, versionar e fazer rollback. - Registrar/listar/remover **end-customers** (as empresas do seu cliente), com isolamento `row_level` ou `schema_per_company`. - Consultar custo/faturamento por projeto. ## Como funciona por dentro (na prática) - **Control-plane.** A API guarda metadados (instâncias, projetos, versões de schema, end-customers) no banco do seu tenant; o banco de dados real fica na GCP. - **Provisionamento assíncrono + keyless.** `POST .../instances` responde na hora com `pending` e enfileira a criação fora do request path. Os adapters GCP reais (Cloud SQL Admin + Resource Manager) rodam com **credencial keyless** (ADC da VM, sem chave guardada). Você faz poll até `ready`. - **SQL editor keyless.** As queries conectam via Cloud SQL Connector + IAM DB auth — nenhuma senha é armazenada. O guard classifica o verbo (`read`/`write`/ `ddl`) e grava o `kind` no histórico. - **Schema engine.** Valida a IR → introspecta o estado atual → planeja o diff (consciente de MySQL 8 vs Postgres) → aplica → grava uma versão **forward-only** (rollback = reaplicar uma versão antiga como uma versão nova). Drops exigem `confirm: true` e vêm com um relatório de impacto (tabelas/colunas dropadas + contagem estimada de linhas). - **Tenancy de end-customers.** O `apply` roteia pela `strategy` do projeto: `row_level` injeta `company_id` nos models `@scope(company)`; `schema_per_company` cria um schema/db por cliente sob demanda (`catcher_c_<…>`), com período de carência antes de dropar na remoção. ## Fluxo típico (end-to-end) ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' # 1. Projeto (uma vez) PID=$(curl -sX POST $B/v1/clouddb/projects -H "$KEY" -H "$JS" \ -d '{"name":"Produção","hosting_mode":"catcher","strategy":"row_level"}' | jq -r .id) # 2. Provisiona a instância (assíncrono) IID=$(curl -sX POST $B/v1/clouddb/instances -H "$KEY" -H "$JS" \ -d "{\"project_id\":\"$PID\",\"engine\":\"postgres\",\"version\":\"POSTGRES_16\",\"region\":\"southamerica-east1\",\"tier\":\"db-custom-2-7680\",\"name\":\"app-db\"}" | jq -r .id) # 3. Poll até ready curl -s $B/v1/clouddb/instances -H "$KEY" | jq '.instances[] | select(.id=="'$IID'") | .status' # 4. Declara o schema (preview → apply) curl -sX POST $B/v1/clouddb/instances/$IID/schema/apply -H "$KEY" -H "$JS" -d '{ "ir": { "format_version":"catcher.schema.v1", "models":[ {"name":"User","table":"users","scope":"global","fields":[ {"name":"id","type":"uuid","primary":true}, {"name":"email","type":"string","unique":true,"size":255}]}]}}' # 5. Roda uma query curl -sX POST $B/v1/clouddb/instances/$IID/query -H "$KEY" -H "$JS" \ -d '{"sql":"SELECT * FROM users LIMIT 10"}' ``` ## Referência de endpoints ### Projetos | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/projects` | Cria projeto (`name`, `hosting_mode`, `strategy`) → `201` | | `GET` | `/v1/clouddb/projects` | Lista projetos | ### Instâncias | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/instances` | Provisiona (`project_id`+`engine`+`version`+`region`+`tier`+`name`) → `202 {id,status:"pending"}` | | `GET` | `/v1/clouddb/instances` | Lista (`status`: `pending`→`provisioning`→`ready`\|`failed`, `last_error`) | `name` da instância: `^[a-z][a-z0-9-]{0,62}$`. `engine`: `mysql`/`postgres`. ### SQL editor | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/instances/{id}/query` | Roda **uma** instrução. Body `{sql, allow_write?, allow_ddl?}` | | `GET` | `/v1/clouddb/instances/{id}/query/history` | Histórico (mais recente primeiro) | Sucesso: `200 {ok:true, columns, rows, row_count, truncated, duration_ms}`. Erro de query (renderizável): `200 {ok:false, error, duration_ms}`. O guard rejeita `;` empilhado, comentários e verbos não-classificados; `allow_write`/`allow_ddl` default `false`. ### Schema engine (`catcher.schema.v1`) | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/instances/{id}/schema/preview` | Valida + retorna o diff, **não aplica** | | `POST` | `/v1/clouddb/instances/{id}/schema/apply` | Aplica + grava versão (`confirm:true` se destrutivo) | | `GET` | `/v1/clouddb/instances/{id}/schema/versions` | Versões (mais recente primeiro) | | `POST` | `/v1/clouddb/instances/{id}/schema/rollback` | Reaplica uma versão antiga como nova (`version`, `confirm`) | IR: `models[]` com `fields[]`; `type` ∈ `string,text,int,uint,decimal,bool,date,datetime,uuid,json`; `string` sem `size` → 255 (> 65535 → use `text`); `scope` `global`|`company`. Destrutivo → `409 SCHEMA_DESTRUCTIVE_REQUIRES_CONFIRM` (com `diff` + `impact`); IR inválida → `422 SCHEMA_INVALID` (com `issues`). ### End-customers (multi-tenancy) | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/clouddb/projects/{pid}/companies` | Registra end-customer (`external_id`, `name`) → `201` | | `GET` | `/v1/clouddb/projects/{pid}/companies` | Lista (+ `strategy` + `schema_name` sob `schema_per_company`) | | `DELETE` | `/v1/clouddb/projects/{pid}/companies/{cid}` | Soft-delete (`?confirm=true` sob `schema_per_company`) → `204` | Sob `schema_per_company` a remoção mantém o schema por `CLOUDDB_COMPANY_REAP_GRACE_DAYS` (default 7) antes do reaper dropar — reversível na janela. Os "end-customers" aqui são os **Tenants** — o mesmo `external_id` (= `tenant_id`) isola esse cliente também no Vector, Storage e Cache. **Quando usar cada estratégia:** | Critério | `row_level` | `schema_per_company` | |---|---|---| | Isolamento | lógico (filtro forçado) | físico (schema separado) | | Ideal para | muitos clientes pequenos/homogêneos | separação forte, auditoria/compliance | | Custo operacional | menor (um schema só) | maior (um schema por cliente) | | Escala de nº de clientes | milhares | dezenas a centenas | A `strategy` é definida **por projeto** (imutável na criação); se precisa das duas, crie dois projetos. Visão transversal do eixo Tenant nos 4 produtos: **[Tenants](tenants.md)**. ### Billing | Método | Rota | O que faz | |---|---|---| | `GET` | `/v1/clouddb/billing/usage?from&to` | Custo GCP + faturado (markup +50% no modo `catcher`). Degrada com `available:false` se o export não estiver configurado | Money em **minor units** (centavos) + `display`, sem float drift. ## Códigos de erro do módulo `INSTANCE_NOT_FOUND` (404) · `INSTANCE_NOT_READY` (409) · `PROJECT_NOT_FOUND` (404) · `SCHEMA_INVALID` (422, com `issues`) · `SCHEMA_DESTRUCTIVE_REQUIRES_CONFIRM` (409, com `diff`+`impact`) · `SCHEMA_VERSION_NOT_FOUND` (404) · `COMPANY_ALREADY_EXISTS` (409) · `COMPANY_NOT_FOUND` (404) · `COMPANY_REMOVE_REQUIRES_CONFIRM` (409) · `MISSING_FIELD` (400). ## Gotchas - **`404` numa rota válida = `CLOUDDB_ENABLED=false`** no ambiente (ou a feature degradou por não conseguir construir os clients GCP). Não é o seu request. - **Provisionar é assíncrono** — não trate o `202` como "pronto"; faça poll até `ready`. - **Drops nunca silenciosos** — `apply`/`rollback` destrutivo só passa com `confirm:true`; leia o `impact` antes. - **SQL editor é control-plane**, uma instrução por vez — não é um pool de conexão para a sua app rodar mil queries; é para administração/inspeção. --- # Vector — busca semântica gerenciada (`cloudvec`) > Rotas `/v1/cloudvec/*` · owner/admin · gated por `CLOUDVEC_ENABLED` (off → 404) ## O que é **Busca semântica gerenciada** (RAG) sobre seus textos e documentos. Você cria **coleções**, coloca documentos (ou **sobe arquivos** que o sistema converte + chunka + indexa), e busca por **significado** — não por palavra-chave. Por baixo: embeddings da OpenAI + PostgreSQL/pgvector. ## O que dá pra fazer - Criar/listar/remover **coleções** (por projeto). - Organizar em **pastas** hierárquicas (`suporte/faturamento/`) e **escopar a busca** por pasta (recursivo). - Adicionar documentos por texto (embeda + upserta em lote, até 100/request). - **Ingerir qualquer arquivo** (PDF, docx, xlsx, pptx, imagens, md, txt…): o pipeline converte para Markdown, guarda original + Markdown no [Storage](storage.md), chunka e embeda. Imagens e PDFs escaneados passam por **OCR via visão**. - **Buscar** por similaridade de cosseno, escopada por pasta; resultados de arquivos apontam de volta para a fonte. ## Como funciona por dentro (na prática) - **Coleção = control-row + tabela de vetores.** Cada coleção é uma linha no tenant DB (`vec_collections`) + uma tabela no pgvector (namespaced pelo `id` da coleção). - **Embedder por-coleção.** O modelo de embedding fica **gravado em cada coleção** (default para novas: `text-embedding-3-large`, 3072 dims; coleções antigas `3-small`/1536 continuam funcionando — modelos coexistem). A busca embeda a query com o modelo da coleção e compara com `<=>` (cosine distance) no pgvector. - **Pastas = caminho materializado.** Cada documento tem um `path`. A busca é por **prefixo, recursiva**: sem `path` busca tudo; `path:"suporte/"` busca `suporte/` + subpastas. Só documentos reais entram (placeholders de pasta vazia são excluídos). - **Ingestão (fontes).** Um arquivo → sidecar **MarkItDown** (+ passe opcional de embelezamento OpenAI) → **Storage** guarda original + `.md` espelhando a árvore de pastas (`vector//`) → o Markdown é quebrado em chunks embedados. Imagens/PDF escaneado → **OCR de visão** (modelo `gpt-5.4-mini`, `detail:high`, prompt de fidelidade que não inventa valores). Cada arquivo vira uma **fonte** (`VecSource`) rastreável; seus chunks não inflam o `document_count` nem aparecem no `GET /documents` — ficam no card da fonte, e carregam `metadata.source_filename`/`source_id` para o resultado apontar de volta. - **Busca híbrida e re-rank.** Além do cosine puro, a busca aceita fusão híbrida (semântica + lexical via RRF) para não perder termo exato, re-rank LLM opt-in, filtros por metadata/fonte/tipo e modos de retrieval com FAQ cards (`qa_first`). Detalhes na seção **Busca** abaixo. ## Fluxo típico (end-to-end) ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' PID= # 1. Cria a coleção (tenant_id é opcional — isola um cliente final; ver Tenants) CID=$(curl -sX POST $B/v1/cloudvec/projects/$PID/collections -H "$KEY" -H "$JS" \ -d '{"name":"docs"}' | jq -r .id) # 2a. Adiciona documentos por texto (embeda em lote) curl -sX POST $B/v1/cloudvec/collections/$CID/documents -H "$KEY" -H "$JS" -d '{ "documents":[{"content":"O servidor tem 12 meses de garantia.","path":"suporte/","metadata":{"src":"faq"}}]}' # 2b. …ou sobe um arquivo (converte → Storage → chunka → embeda; assíncrono) curl -sX POST $B/v1/cloudvec/collections/$CID/ingest -H "$KEY" \ -F "file=@./manual.pdf" -F "path=suporte/" # → { "id":"…","status":"processing","chunk_count":…,"filename":"manual.pdf" } # 3. Busca semântica (escopada por pasta, recursiva) curl -sX POST $B/v1/cloudvec/collections/$CID/search -H "$KEY" -H "$JS" \ -d '{"query":"quanto tempo de cobertura contra defeitos?","k":5,"path":"suporte/"}' # → { "results":[{"id","content","metadata","score"}], "count":5 } ``` ## Referência de endpoints ### Coleções | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudvec/projects/{pid}/collections` | Cria (`{name}` lowercase 2-48, `tenant_id?`) → `201` (`embed_model`, `dimensions`, `tenant_id?`) | | `GET` | `/v1/cloudvec/projects/{pid}/collections` | Lista | | `GET` | `/v1/cloudvec/collections/{id}` | Detalhe + `document_count` vivo | | `DELETE` | `/v1/cloudvec/collections/{id}` | Remove (dropa tabela de vetores + control-row) → `204` | #### Isolamento por tenant (end-customers) Passe `tenant_id` ao criar a coleção para isolar um **cliente final**: a coleção passa a viver no **schema Postgres dedicado do tenant** (`t_` + role `tr_`) e a busca naquela coleção só enxerga os vetores dele. Sem `tenant_id`, a coleção é de nível projeto (schema default `public`, compartilhada). É o mesmo `tenant_id` usado no Database, Storage e Cache — ver **[Tenants](tenants.md)**. Para isolar ainda mais fino (por usuário final dentro de um tenant) sem multiplicar coleções, use o `filter` por metadata na busca (abaixo). ### Pastas | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudvec/collections/{id}/folders` | Cria pasta (`{path}`) → `201 {path:"suporte/faturamento/"}` | | `GET` | `/v1/cloudvec/collections/{id}/folders?parent=` | Subpastas imediatas | | `GET` | `/v1/cloudvec/collections/{id}/folders/stats?path=` | `{document_count}` recursivo | | `DELETE` | `/v1/cloudvec/collections/{id}/folders?path=` | Apaga pasta + conteúdo recursivo → `{deleted:N}` | ### Documentos | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudvec/collections/{id}/documents` | Embeda + upserta (`{documents:[{content,path?,id?,metadata?}]}`, máx. 100) → `201` | | `GET` | `/v1/cloudvec/collections/{id}/documents?path=&recursive=&limit=` | Lista (limit ≤ 1000) | | `DELETE` | `/v1/cloudvec/collections/{id}/documents/{docId}` | Remove → `204` | ### Busca `POST /v1/cloudvec/collections/{id}/search` → `{results:[{id,content,metadata,score}], count}` | Campo | Tipo | O que faz | |---|---|---| | `query` | string | A consulta (é embedada — 1 embedding por busca). | | `k` | int | Máximo de resultados (default 5). | | `path` | string | Escopo por pasta, recursivo (`""`/ausente = coleção inteira). | | `hybrid` | bool | **Fusão RRF** do braço semântico (cosine) com um braço lexical full-text ANTES do corte top-k — um doc term-exato (ID, SKU, código) que o cosine enterrou ainda aparece. Cada hit ganha `metadata.match: "semantic"\|"lexical"\|"both"`. | | `alpha` | float 0..1 | Peso do braço semântico na fusão (lexical = 1−alpha; ausente = 0.5; `0` = lexical puro). | | `rerank` | bool | Re-rank listwise com LLM após a recuperação (opt-in — custa uma chamada de modelo; use em consultas críticas). | | `mode` | string | Retrieval com FAQ cards do Refinery: default `qa_first` (cards antes de chunks; degrada para chunks se não houver cards) · `chunks` (só chunks) · `qa_only`. | | `filter` | map | Pré-filtro de igualdade por metadata (AND), ex.: `{"end_user_id":"u_9"}`. | | `source_id` | string | Só chunks de uma fonte específica. | | `type` | string | Só um tipo de fonte (`pdf\|word\|spreadsheet\|markdown\|text\|…`). | `score` = similaridade de cosseno em [0,1] (1 = idêntico). ### Ingestão de arquivos (fontes) | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudvec/collections/{id}/ingest` | Multipart (`file` + `path?`), ≤ 64 MB → `{id,status:"processing",chunk_count,…}` | | `GET` | `/v1/cloudvec/collections/{id}/sources?path=` | Lista fontes em `path` | | `GET` | `/v1/cloudvec/collections/{id}/sources/{sid}` | Detalhe + **signed URLs** (TTL 30 min) do original + Markdown + os chunks | | `DELETE` | `/v1/cloudvec/collections/{id}/sources/{sid}` | Remove fonte + chunks + objetos no Storage → `204` | Ingestão de binários requer o sidecar (`CLOUDVEC_CONVERTER_URL`) + `CLOUDSTORE_ENABLED` + um GCP project READY no projeto (para o bucket `vector-sources`). `.md`/`.txt` passam direto. ## Códigos de erro do módulo `INVALID_COLLECTION_NAME` (400, inclui `path` inválido) · `COLLECTION_ALREADY_EXISTS` (409) · `COLLECTION_NOT_FOUND` (404) · `EMPTY_INPUT` (400) · `BATCH_TOO_LARGE` (400, > 100 docs) · `EMBEDDING_DIMENSION_MISMATCH` (400) · `SOURCE_NOT_FOUND` (404) · `CONVERSION_UNAVAILABLE` (422, sidecar ausente p/ binário) · `NO_READY_PROJECT` (409) · `MISSING_FIELD` (400). ## Gotchas - **Embeddings custam** (OpenAI) — `documents` cobra por embedar cada `content`; busca cobra 1 embedding por query. Agrupe inserções em lote (até 100/request). - **Não misture modelos numa coleção** — o modelo é fixado na criação; se mudar o default global, coleções novas usam o novo, as antigas mantêm o seu (sem `EMBEDDING_DIMENSION_MISMATCH`). - **Ingestão é assíncrona** — `status: processing` → `ready`|`failed` (`last_error`); faça poll via `GET .../sources/{sid}`. - **Chunks de fontes ≠ documents** — não aparecem no `GET /documents` nem contam no `document_count`; aparecem nos resultados de busca com `metadata.source_filename`. - **Para RAG:** ingira os docs em pastas por assunto, depois `search` com `path` para focar o contexto e `k` para o tamanho do contexto. --- # Cache — Redis gerenciado (`cloudcache`) > Rotas `/v1/cloudcache/*` · owner/admin · gated por `CLOUDCACHE_DEDICATED_ENABLED` > (off → `404 CLOUDCACHE_DEDICATED_UNAVAILABLE`) ## O que é **Redis gerenciado**, **dedicated-only**: cada cliente que quer cache provisiona a **própria instância Redis numa VM dedicada** (core/RAM/eviction/falha isolados, não compartilha com ninguém). Igual ao Database (decisão D1), é **control-plane only** — o seu app conecta **direto** no DSN via qualquer cliente Redis (`redis.UniversalClient`, ioredis, etc.); a Catcher **não** faz proxy de comando, então não há latência extra no caminho quente. > O modelo *shared* anterior (namespaces ACL sobre tiers `ephemeral`/`retained` > compartilhados) foi **aposentado em 2026-06-25**. As rotas > `…/cloudcache/namespaces*` não existem mais — hoje é só dedicated. ## O que dá pra fazer - **Estimar o custo** mensal antes de provisionar (dry-run, não cria nada). - **Provisionar** uma VM Redis dedicada (assíncrono) — a senha aparece **uma vez**. - **Listar / detalhar** suas instâncias (status + `dsn_host` quando `ready`). - **Redimensionar** (muda máquina/RAM), **pausar/religar** (corta o custo de compute), **reconciliar** o status com a VM real, **destruir**. - Ver **métricas** da VM (CPU/memória/etc.). ## Como funciona por dentro (na prática) - **Control-plane + VM real.** A API guarda a control-row no tenant DB; o Redis roda numa **VM Debian** dedicada (startup-script instala + configura o Redis por modo + hardening), **sem service account** (zero credencial na VM), servindo Redis **TLS (`rediss://`) na porta `6380`** com um cert **Let's Encrypt** para o host brandado da VM (DNS próprio, ou `.sslip.io` como fallback). - **Dois modos:** - **`mem_fast`** — `noeviction`, dimensionado por RAM, sub-ms, **mais caro**. Para cache que não pode perder chave sob pressão. - **`economico`** — `allkeys-lru`, RAM menor, **mais barato** — um miss cai no banco. É **eviction, não swap**. - **Provisionamento assíncrono.** `POST …/dedicated` responde com a instância `pending`/`provisioning` + a **senha (uma única vez)**; você faz poll de `GET …/dedicated/{id}` até `ready`, quando o `dsn_host` aparece. - **Conexão direta, por TLS.** A resposta traz `dsn_host` + `tls`. O endpoint é um **`rediss://` TLS roteável** (porta `6380`, cert Let's Encrypt, host brandado), então o DSN é `rediss://default:@` e o app conecta **de qualquer lugar** por TLS. (Um host interno plaintext usaria o esquema `redis://`.) ## Fluxo típico (end-to-end) ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' # 1. Veja o custo ANTES de criar (dry-run, não provisiona) curl -sX POST $B/v1/cloudcache/dedicated/estimate -H "$KEY" -H "$JS" \ -d '{"mode":"mem_fast","machine":"e2-medium","ram_mb":4096,"disk_gb":20,"region":"southamerica-east1"}' # → { "total_usd": 31.80, ... } # 2. Provisiona (assíncrono) — a senha vem UMA vez, guarde já curl -sX POST $B/v1/cloudcache/dedicated -H "$KEY" -H "$JS" \ -d '{"name":"prod","mode":"economico","machine":"e2-micro","ram_mb":1024,"disk_gb":20,"region":"southamerica-east1"}' # → { "instance": { "id":"…","status":"provisioning",… }, "password":"…" } # 3. Poll até ready (o dsn_host aparece quando pronto) curl -s $B/v1/cloudcache/dedicated/ -H "$KEY" | jq '.status, .dsn_host' # 4. No seu app: conecta direto por TLS (DSN = rediss://default:@) # e prefixe as chaves por tenant — ver a seção abaixo. ``` ## Referência de endpoints | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudcache/dedicated/estimate` | Custo $/mês (`{mode,machine,ram_mb,disk_gb,region}`) — dry-run, não cria | | `POST` | `/v1/cloudcache/dedicated` | Provisiona (async) → `{instance, password}` (**senha uma vez**) | | `GET` | `/v1/cloudcache/dedicated` | Lista as instâncias | | `GET` | `/v1/cloudcache/dedicated/{id}` | Detalhe (status + `dsn_host` quando `ready`) | | `GET` | `/v1/cloudcache/dedicated/{id}/metrics` | Métricas da VM (CPU/memória/…) | | `POST` | `/v1/cloudcache/dedicated/{id}/resize` | Muda máquina/modo (`{machine,ram_mb}`; stop→resize→start) | | `POST` | `/v1/cloudcache/dedicated/{id}/stop` · `/start` | Pausa (corta custo de compute) / religa | | `POST` | `/v1/cloudcache/dedicated/{id}/reconcile` | Re-sincroniza o status com a VM real (não-disruptivo) | | `DELETE` | `/v1/cloudcache/dedicated/{id}` | Destrói a VM + a control-row | O custo aparece na **ativação ANTES de confirmar** (ex.: `e2-micro` econômico ~US$ 10,20/mês · `e2-medium` mem-fast ~US$ 31,80/mês). ## Isolamento por tenant (end-customers) Como o Cache é control-plane only (você conecta direto no DSN; não há proxy de comando), o isolamento por **cliente final** é por **convenção**, aplicada no SEU código: 1. **Prefixe** toda chave de um tenant com `t::` — ex. `t:acme:sessao:123`. 2. (Recomendado) crie um **usuário ACL do Redis por tenant**, restrito ao padrão `~t::*`, de modo que aquele tenant não leia/escreva fora do próprio prefixo. É o **mesmo `tenant_id`** que você usa no Database, Vector e Storage — o eixo transversal. Modelo completo + recomendações: **[Tenants](tenants.md)**. ## Códigos de erro do módulo `CLOUDCACHE_DEDICATED_UNAVAILABLE` (404 — feature desligada no ambiente, ou o wiring de GCP Compute não pôde ser construído). ## Gotchas - **A senha aparece uma única vez** (na criação) — guarde no seu secret manager. Para trocar, redimensione/recrie conforme a necessidade. - **Provisionar é assíncrono** — não trate a criação como pronta; faça poll de `GET …/dedicated/{id}` até `ready` (o `dsn_host` só aparece então). - **Endpoint TLS público (`rediss://` :6380)** — host brandado com cert Let's Encrypt; a conexão é criptografada. O esquema vem indicado pelo campo `tls` na resposta (`rediss://` quando público-TLS, `redis://` se host interno plaintext). - **`economico` é eviction, não swap** — sob pressão, chaves são descartadas (`allkeys-lru`) e o miss cai no banco. Use `mem_fast` quando não pode perder chave. - **`stop` corta o custo de compute** mas mantém a instância — útil para ambientes que não rodam 24/7. `reconcile` conserta uma control-row presa em `failed` após timeout de uma operação de control-plane, sem reiniciar a VM. --- # Storage — objetos gerenciados (`cloudstore`) > Rotas `/v1/cloudstore/*` · owner/admin · gated por `CLOUDSTORE_ENABLED` (off → 404) ## O que é **Buckets de objetos gerenciados** em cima do Google Cloud Storage. Você cria buckets por projeto e faz upload/download/organização de arquivos pela API — sem tocar no console da GCP. Os buckets ficam no **mesmo GCP project** que hospeda as instâncias de [Database](database.md) daquele projeto. ## O que dá pra fazer - Criar/listar/remover buckets; ver o **overview** (contagem de objetos, bytes totais, settings ao vivo do GCS). - Upload (multipart), download (stream autenticado **ou** signed URL keyless), delete. - **Pastas** (prefixos `foo/bar/`): criar, ver stats (count + bytes), apagar recursivamente. - **Power-ops** de objeto: `stat` (metadata completa), `copy`, `move`/rename, `meta` (editar content-type/cache/metadata/storage-class). - **Acesso S3-compatível**: emitir chaves HMAC e apontar qualquer SDK do S3/R2 para `storage.googleapis.com`. ## Como funciona por dentro (na prática) - **Control-plane + GCS real.** A API guarda a control-row do bucket no tenant DB; o objeto vive no GCS. O nome real do bucket é globalmente único (`ctc--`) — você sempre usa o `id` da control-row. - **Signed URLs keyless (V4).** `objects/sign` devolve uma URL que o GCS serve **direto** (`storage.googleapis.com`), sem passar pelo Catcher nem expor credencial — ideal para o navegador baixar/exibir. Default TTL 15 min, máx. 7 dias. - **Pastas são prefixos.** O GCS não tem pastas; uma "pasta" é um placeholder zero-byte com chave terminada em `/`. Os stats e a varredura ignoram o placeholder. - **Content-Type normalizado** no upload: tipos textuais ganham `; charset=utf-8` e tipos vazios/`octet-stream` são inferidos da extensão — a signed URL renderiza UTF-8 no navegador sem mojibake. - **S3-compat isolado por cliente:** cada projeto usa uma SA dedicada (`ctc-store@`) com acesso só ao próprio projeto; o `secret` da chave HMAC só aparece na criação. ## Fluxo típico (end-to-end) ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' PID= # mesmo projeto do Database (ver Getting Started) # 1. Cria o bucket (tenant_id é opcional — dedica o bucket a um cliente final; ver Tenants) BID=$(curl -sX POST $B/v1/cloudstore/projects/$PID/buckets -H "$KEY" -H "$JS" \ -d '{"name":"assets","location":"southamerica-east1","storage_class":"STANDARD"}' | jq -r .id) # 2. Upload (multipart). 'key' opcional aceita prefixo (cria a "pasta") curl -sX POST $B/v1/cloudstore/buckets/$BID/objects -H "$KEY" \ -F "file=@./logo.png" -F "key=brand/logo.png" # 3. Lista sob um prefixo curl -s "$B/v1/cloudstore/buckets/$BID/objects?prefix=brand/" -H "$KEY" # 4. Signed URL para o navegador baixar/exibir (15 min) curl -s "$B/v1/cloudstore/buckets/$BID/objects/sign?key=brand/logo.png&ttl=900" -H "$KEY" # → { "url": "https://storage.googleapis.com/…", "expires_in": 900 } ``` ## Referência de endpoints ### Buckets | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudstore/projects/{pid}/buckets` | Cria (`name`, `location?`, `storage_class?`, `tenant_id?`) → `201` | | `GET` | `/v1/cloudstore/projects/{pid}/buckets` | Lista | | `GET` | `/v1/cloudstore/buckets/{id}/overview` | `object_count`+`total_bytes` + settings GCS (versioning, public-access-prevention, UBLA) | | `DELETE` | `/v1/cloudstore/buckets/{id}` | Remove (GCS + control-row) → `204` | #### Isolamento por tenant (end-customers) Passe `tenant_id` ao criar o bucket para dedicá-lo a um **cliente final**: o bucket fica isolado por IAM no GCS — um bucket por cliente. Sem `tenant_id`, o bucket é de nível projeto (compartilhado entre seus clientes). É o mesmo `tenant_id` usado no Database, Vector e Cache — ver **[Tenants](tenants.md)**. ### Objetos | Método | Rota | O que faz | |---|---|---| | `GET` | `/v1/cloudstore/buckets/{id}/objects?prefix=&limit=` | Lista (`key,size,content_type,updated,etag`) | | `POST` | `/v1/cloudstore/buckets/{id}/objects` | Upload multipart (`file` + `key?`) | | `DELETE` | `/v1/cloudstore/buckets/{id}/objects?key=` | Remove → `204` | | `GET` | `/v1/cloudstore/buckets/{id}/objects/sign?key=&ttl=` | Signed URL (download direto do GCS) | | `GET` | `/v1/cloudstore/buckets/{id}/objects/download?key=` | Stream pelo control-plane (autenticado) | | `GET` | `/v1/cloudstore/buckets/{id}/objects/stat?key=` | Metadata completa (generation, md5, crc32c, custom `x-goog-meta-*`, …) | | `POST` | `/v1/cloudstore/buckets/{id}/objects/copy` | Duplica (`{src,dst}`, rewrite server-side) → `201` | | `POST` | `/v1/cloudstore/buckets/{id}/objects/move` | Move/rename (`{src,dst}`, copy+delete) → `200` | | `PATCH` | `/v1/cloudstore/buckets/{id}/objects/meta?key=` | Edita content-type/cache/disposition/language/storage-class/metadata | ### Pastas | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudstore/buckets/{id}/folders` | Cria pasta vazia (`{path}`) → `201 {path:"docs/sub/"}` | | `GET` | `/v1/cloudstore/buckets/{id}/folders/stats?path=` | `{count, total_bytes}` sob o prefixo | | `DELETE` | `/v1/cloudstore/buckets/{id}/folders?path=` | Apaga recursivamente tudo sob o prefixo → `{deleted:N}` | ### Acesso S3-compatível (HMAC) | Método | Rota | O que faz | |---|---|---| | `POST` | `/v1/cloudstore/projects/{pid}/hmac-keys` | Emite chave (`secret` só aqui) | | `GET` | `/v1/cloudstore/projects/{pid}/hmac-keys` | Lista | | `DELETE` | `/v1/cloudstore/projects/{pid}/hmac-keys/{accessId}` | Revoga | Aponte qualquer SDK do S3/R2 para `https://storage.googleapis.com` com a chave/secret. ## Códigos de erro do módulo `INVALID_BUCKET_NAME` (400) · `BUCKET_ALREADY_EXISTS` (409) · `BUCKET_NOT_FOUND` (404) · `BUCKET_NOT_READY` (409) · `OBJECT_NOT_FOUND` (404) · `NO_READY_PROJECT` (409, sem GCP project pronto no projeto) · `S3_ACCESS_DISABLED_BY_POLICY` (409, org policy bloqueia chaves de SA). ## Gotchas - **Use sempre o `id` da control-row**, não o nome real do GCS (`ctc-…`). - **Para o navegador, prefira `sign`** (download direto, sem custo de proxy); `download` é o stream autenticado pelo control-plane (útil server-side). - **Apagar pasta é recursivo e definitivo** — `path` não pode ser vazio/`/`/`.`/`..` (guard contra apagar o bucket inteiro). - **`move` não é atômico** (copy + delete) — em falha no meio, pode sobrar a origem. - **`NO_READY_PROJECT`** → o projeto ainda não tem um GCP project provisionado (geralmente porque nenhuma instância de Database foi criada nele ainda). --- # Wiki — base de conhecimento com RAG (`cloudwiki`) > Rotas `/v1/cloudwiki/*` · owner/admin · gated por `CLOUDWIKI_ENABLED` (off → 404) ## O que é Uma **base de conhecimento gerenciada**: espaços de páginas em Markdown (frontmatter + corpo), organizados em pastas, com **busca híbrida** (semântica + lexical), **perguntas com resposta fundamentada** (`ask`, RAG com fontes citadas), **ingestão de arquivos** e o **Knowledge Refinery** — um destilador assíncrono que transforma o espaço em FAQ cards. Cada página é indexada **na escrita** (sem passo de rebuild). ## O que dá pra fazer - Criar **espaços** por projeto (opcionalmente por tenant) e organizá-los em **pastas** numa árvore. - **Upsert de páginas** por path (`PUT …/pages/`) com Markdown — a página fica pesquisável na hora. - **Buscar** (`search`) com fusão híbrida RRF e **perguntar** (`ask`) com resposta fundamentada + páginas-fonte. - **Ingerir arquivos** (PDF, DOCX, MD…) como fontes do espaço, com re-chunk, habilitar/desabilitar e reindexar. - Rodar o **Refinery** (FAQ cards destilados) e listar os cards. - Auditar: **graph** de wikilinks (resolvidos/quebrados) e **audit** (páginas sem fonte, links quebrados, órfãs). ## Fluxo em 5 minutos ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' # 1. Criar um espaço no projeto SID=$(curl -sX POST $B/v1/cloudwiki/projects/$PID/spaces -H "$KEY" -H "$JS" \ -d '{ "name": "Base de conhecimento" }' | jq -r .id) # 2. Escrever uma página (indexada na hora) curl -sX PUT "$B/v1/cloudwiki/spaces/$SID/pages/politicas/reembolso" -H "$KEY" -H "$JS" \ -d '{ "markdown": "---\ntitle: Política de reembolso\n---\n\n# Política de reembolso\n\nPlano anual: 30 dias…" }' # 3. Buscar (híbrido) curl -sX POST $B/v1/cloudwiki/spaces/$SID/search -H "$KEY" -H "$JS" \ -d '{ "q": "reembolso plano anual", "hybrid": true, "limit": 5 }' # 4. Perguntar (RAG com fontes) curl -sX POST $B/v1/cloudwiki/spaces/$SID/ask -H "$KEY" -H "$JS" \ -d '{ "question": "qual o prazo de reembolso do plano anual?" }' # → { "answer": "…", "sources": [{ "path": "politicas/reembolso", … }] } # 5. Ingerir um arquivo como fonte (multipart) curl -sX POST $B/v1/cloudwiki/spaces/$SID/sources -H "$KEY" \ -F "file=@contrato-padrao.pdf" ``` ## Busca e ask — parâmetros `POST /v1/cloudwiki/spaces/{sid}/search`: | Campo | Tipo | O que faz | |---|---|---| | `q` | string | A consulta. | | `hybrid` | bool | Funde o braço semântico (cosine/pgvector) com o lexical (FTS) por **RRF** antes do corte — recomendado; resolve termo exato (ID, SKU, código) que o cosine enterra. | | `qa_first` | bool | Prefere FAQ cards destilados (Refinery) antes de chunks crus. | | `limit` | int | Máximo de hits. | | `lexical_weight` | float 0..1 | Inclina a fusão para o braço lexical (default 0.5). | | `rerank` | bool | Re-rank listwise com LLM dos candidatos over-fetched (opt-in — custa uma chamada de modelo). | | `k_overfetch` | int | Quantos candidatos buscar antes do re-rank (default 20). | | `path_prefixes` | []string | Escopa a busca a paths que começam com os prefixos (ex.: `["politicas/"]`). | Cada hit informa **como** foi encontrado: `match: "semantic" | "lexical" | "both"` — debug de retrieval é parte do contrato. `POST /v1/cloudwiki/spaces/{sid}/ask` aceita `question` + os mesmos ajustes (`qa_first`, `lexical_weight`, `rerank`, `k_overfetch`, `path_prefixes`); a recuperação do RAG é sempre híbrida. A resposta traz `answer` + `sources`. ## Fontes (arquivos ingeridos) | Rota | O que faz | |---|---| | `POST /spaces/{sid}/sources` (multipart `file=`) | Ingere: converte para Markdown, chunka e indexa. | | `GET /spaces/{sid}/sources` | Lista (filename, status, chunks, enabled). | | `GET /spaces/{sid}/sources/{srcid}` | Detalhe: Markdown convertido + chunks. | | `PATCH /spaces/{sid}/sources/{srcid}` `{enabled}` | Liga/desliga a fonte no retrieval (desabilitada = guardada mas fora da busca). | | `POST /spaces/{sid}/sources/{srcid}/reindex` | Re-chunk + re-embed mantendo o id. | | `DELETE /spaces/{sid}/sources/{srcid}` | Remove a fonte e seus chunks. | ## Knowledge Refinery (FAQ cards) O Refinery destila o espaço em **cards de pergunta-resposta** com fontes — um processo assíncrono com status acompanhável: ```bash curl -sX POST $B/v1/cloudwiki/spaces/$SID/refinery/runs -H "$KEY" # dispara curl -s $B/v1/cloudwiki/spaces/$SID/refinery/runs -H "$KEY" # status dos runs curl -s $B/v1/cloudwiki/spaces/$SID/faq -H "$KEY" # cards prontos ``` Com `qa_first: true` na busca/ask, perguntas recorrentes acertam o card direto em vez de costurar chunks. Config por espaço em `GET/PATCH /spaces/{sid}/refinery/config`. ## Organização e governança - **Árvore**: `GET /projects/{pid}/tree` (pastas + espaços); `POST/GET/DELETE /projects/{pid}/folders`; `PATCH /spaces/{sid}` renomeia/move. - **Graph**: `GET /spaces/{sid}/graph` — arestas de wikilinks (`[[caminho/da-pagina]]`), resolvidos e quebrados. - **Audit**: `GET /spaces/{sid}/audit` — páginas sem fonte, links quebrados, órfãs. Meta saudável: tudo zero. ## Boas práticas - **Páginas são síntese**, não transcript: uma página por conceito, com wikilinks entre vizinhas — o graph/audit é seu detector de deriva. - **Escreva na mesma operação em que aprendeu**: a página já sai indexada; não existe "rebuild depois". - Para agentes de IA, exponha o espaço via **[MCP](mcp.md)** (`wiki_ask`, `wiki_search`, `wiki_upsert_page`) com escopos `wiki:read`/`wiki:write`. --- # MCP — seus dados como ferramentas para agentes (`cloudmcp`) > Endpoint do protocolo: `POST /v1/mcp` (Streamable HTTP) · gestão em `/v1/mcp/*` (owner/admin) ## O que é Um endpoint **Model Context Protocol** que expõe os produtos da sua conta como **ferramentas tipadas** (~54 tools) para Claude Code, Cursor e qualquer client MCP: perguntar à wiki, buscar nos vetores, rodar SQL governado, listar/assinar arquivos do storage, inspecionar o cache. O agente trabalha com seus dados — **sem credencial de banco em prompt** — e cada chamada passa por token com escopo, RBAC por usuário e SQL guard. ## Conectar em 1 comando ```bash claude mcp add --transport http catcher-data \ https://data-api.catcher.one/v1/mcp \ --header "Authorization: Bearer ctc_mcp_SEU_TOKEN" ``` Qualquer client compatível funciona: a autenticação é `Authorization: Bearer` com um **token MCP** (abaixo). Um `401` devolve o **Protected Resource Metadata (RFC 9728)**, então clients modernos descobrem sozinhos como autenticar. ## O manifest é o contrato ```bash curl -s https://data-api.catcher.one/v1/mcp/manifest -H "X-API-Key: ctc_…" ``` Retorna o `endpoint`, o `connect_hint` (o comando pronto acima), o catálogo de `scopes` e a lista de `tools` — cada tool com `name`, `scope` exigido e `description`. O Console (página **MCP**) mostra o mesmo catálogo. ## Tokens MCP (PAT por usuário) Tokens do MCP são **separados das API keys**: prefixo `ctc_mcp_`, emitidos por usuário, com **escopos** e TTL opcional. ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' # mintar (o segredo aparece UMA única vez) curl -sX POST $B/v1/mcp/tokens -H "$KEY" -H "$JS" \ -d '{ "label": "claude-code", "scopes": ["wiki:read","vec:read","db:read"], "expires_in_days": 30 }' # → { "id": 7, "token": "ctc_mcp_…", "scopes": [...], ... } curl -s $B/v1/mcp/tokens -H "$KEY" # listar (label, last4, escopos, último uso, status) curl -sX DELETE $B/v1/mcp/tokens/7 -H "$KEY" # revogar — efeito imediato ``` ### Escopos | Escopo | Libera | |---|---| | `wiki:read` / `wiki:write` | ask, search, get/list de páginas / upsert, delete, ingestão, refinery | | `vec:read` / `vec:write` | busca e listagens / criar coleção, adicionar docs, ingerir | | `db:read` / `db:write` | `db_run_sql` read-only / escrita (DDL continua sujeito ao guard) | | `store:read` / `store:write` | listar, stat, URL assinada / upload, delete | | `cache:read` | inspeção do cache | Um token só enxerga as tools cujos escopos carrega — o resto nem aparece na listagem de tools daquele token. ## RBAC por usuário (overrides) Além dos escopos do token, o owner/admin pode liberar ou bloquear um escopo para um usuário específico — e **deny sempre vence**: ```bash curl -sX PUT $B/v1/mcp/grants -H "$KEY" -H "$JS" \ -d '{ "user_id": 12, "scope": "db:read", "effect": "deny" }' curl -s "$B/v1/mcp/grants?user_id=12" -H "$KEY" curl -sX DELETE "$B/v1/mcp/grants?user_id=12&scope=db:read" -H "$KEY" ``` ## Camadas de defesa 1. **Escopo do token** — `wiki:read` não escreve, nunca. 2. **RBAC por usuário** — deny vence allow; auditável no Console. 3. **SQL guard** — `db_run_sql` nasce read-only; escrita/DDL são liberações explícitas; `GRANT`, `REVOKE` e `CREATE USER` são bloqueados SEMPRE. 4. **Isolamento de conta/tenant** — o token carrega a empresa; ids forjados de outra conta resolvem 404 (validado pela suite de pentest, incluindo os fluxos do MCP). ## Boas práticas - **Um token por agente/ferramenta**, com o menor conjunto de escopos que resolve — revogação cirúrgica quando precisar. - Use TTL (`expires_in_days`) para agentes experimentais. - Prefira `wiki_ask`/`vec_search` a `db_run_sql` quando a pergunta é de conhecimento — resposta melhor, superfície menor. - O guia completo de tools por produto está nas páginas de cada módulo: [Wiki](wiki.md) · [Vector](vector.md) · [Database](database.md) · [Storage](storage.md) · [Cache](cache.md). --- # Tenants — isolando seus clientes finais (o eixo transversal) > O **Tenant** é o cliente do SEU cliente. Um mesmo `tenant_id` isola um cliente > final em TODOS os produtos de dados — [Database](database.md), [Vector](vector.md), > [Storage](storage.md) e Cache. Esta página é o modelo mental que amarra os quatro. ## O modelo: Conta → Projeto → Tenant Tudo no Catcher Data se organiza em **três níveis**: | Nível | O que é | Quem define | |---|---|---| | **Conta** (workspace) | Sua empresa dentro do Catcher Data — onde você faz login / de onde sai a API key. | Catcher (no cadastro) | | **Projeto** | Agrupamento dentro da conta; tem infra própria (instâncias, coleções, buckets, cache) + billing + estratégia de tenancy. Crie via `POST /v1/clouddb/projects`. | Você | | **Tenant** (end-customer) | **O cliente do seu cliente** — um identificador (`tenant_id` / `external_id`) que VOCÊ escolhe para isolar cada cliente final. | Você | > **Terminologia.** O cliente final se chama **Tenant** em todo o produto (Console, > API, manuais). Em rotas mais antigas do Database ele aparece como > "company"/"end-customer" (campo `external_id`) — é o mesmo conceito que o > `tenant_id` dos demais produtos. No Console, a antiga aba "Clientes" agora é > **Tenants**. ## O eixo transversal O `tenant_id` é simplesmente **uma string que você escolhe** para cada cliente final (um id interno, CNPJ, um slug como `"acme"`). Não é um recurso separado a gerenciar — é uma **etiqueta** que você carrega. Use o **MESMO `tenant_id`** em todos os produtos e cada um aplica o isolamento **físico** nativo daquela tecnologia: ``` Projeto ┌──────────────┬───────────────┬───────────────┬───────────────┐ Database Vector Storage Cache │ │ │ │ tenant "acme" ──┼───────────────┼───────────────┼────────────── (o MESMO tenant_id) schema dedicado schema t_acme bucket dedicado chaves t:acme:* ou company_id + role tr_acme (IAM) + ACL ~t:acme:* ``` | Produto | Como o Tenant é isolado | Como você ativa | |---|---|---| | **Database** ([clouddb](database.md)) | schema dedicado por tenant (`schema_per_company`) **ou** coluna `company_id` + filtro forçado (`row_level`) | registre o tenant em `POST /v1/clouddb/projects/{pid}/companies`; o isolamento segue a `strategy` do projeto | | **Vector** ([cloudvec](vector.md)) | schema Postgres dedicado `t_` + role de acesso `tr_` | informe `tenant_id` ao criar a coleção | | **Storage** ([cloudstore](storage.md)) | bucket GCS dedicado, isolado por IAM | informe `tenant_id` ao criar o bucket | | **[Cache](cache.md)** (cloudcache) | prefixo de chave `t::*` + ACL Redis `~t::*` | prefixe suas chaves no app (+ usuário ACL por tenant) | Em todos os casos, **`tenant_id` vazio/ausente = recurso de nível projeto** (compartilhado entre os seus clientes) — apropriado para dados que são SEUS, não de um cliente específico. ## Como ativar em cada produto ### Database — registre o tenant no projeto ```bash KEY="X-API-Key: ctc_…" ; B=https://data-api.catcher.one ; JS='Content-Type: application/json' PID= curl -sX POST $B/v1/clouddb/projects/$PID/companies -H "$KEY" -H "$JS" \ -d '{"external_id":"acme","name":"Acme S.A."}' ``` Sob `schema_per_company`, registrar provisiona o schema do tenant (`catcher_c_…`) sob demanda. Sob `row_level`, o tenant é registry-only e o app filtra por `external_id` (a coluna `company_id`). Detalhes + endpoints: [Database](database.md). ### Vector — `tenant_id` na criação da coleção ```bash curl -sX POST $B/v1/cloudvec/projects/$PID/collections -H "$KEY" -H "$JS" \ -d '{"name":"docs","tenant_id":"acme"}' ``` A coleção passa a viver no schema dedicado do tenant; a busca naquela coleção só enxerga os vetores dele. Detalhes: [Vector](vector.md). ### Storage — `tenant_id` na criação do bucket ```bash curl -sX POST $B/v1/cloudstore/projects/$PID/buckets -H "$KEY" -H "$JS" \ -d '{"name":"assets","location":"southamerica-east1","tenant_id":"acme"}' ``` O bucket fica dedicado ao tenant (isolado por IAM no GCS). Detalhes: [Storage](storage.md). ### Cache — prefixo + ACL no seu código O Cache é control-plane only (seu app conecta direto no DSN; não há proxy de comando), então o isolamento por tenant é por **convenção**, aplicada no SEU código: ```text chave = "t:" + tenant_id + ":" + sua_chave → t:acme:sessao:123 ``` Opcionalmente, crie um **usuário ACL do Redis por tenant**, restrito ao padrão `~t::*`, para que aquele tenant não leia/escreva fora do próprio prefixo. Referência do Cache: **[Cache](cache.md)**. ## Database: qual estratégia escolher? A escolha entre as duas estratégias do Database é feita **por projeto** (campo `strategy` em `POST /v1/clouddb/projects`): | Critério | `row_level` | `schema_per_company` | |---|---|---| | Isolamento | lógico (filtro forçado) | físico (schema separado) | | Ideal para | muitos clientes pequenos/homogêneos; SaaS típico | separação forte, auditoria/compliance, exportar/excluir 1 cliente isoladamente | | Custo operacional | menor (um schema só) | maior (um schema por cliente) | | Escala de nº de clientes | milhares | dezenas a centenas | | "Apagar tudo de um cliente" | apagar as linhas dele | descartar o schema dele | Regra prática: **comece com `row_level`** se você tem muitos clientes parecidos; escolha **`schema_per_company`** quando um cliente exige separação forte por contrato/regulação/auditoria. Se precisa das duas, crie dois projetos. ## Recomendações 1. **`tenant_id` estável e opaco** — use um id que nunca muda (não o nome comercial). É a cola que liga os 4 produtos; se muda, a ligação se perde. 2. **O MESMO `tenant_id` em todos os produtos** — `"acme"` no banco, no vetor, no storage e no cache. Consistência = sistema fácil de auditar. 3. **Um projeto por ambiente/produto SEU, nunca por cliente final** — `Produção`, `Staging`. Cliente final é Tenant DENTRO do projeto, não um projeto. Projetos são poucos e duradouros; tenants são muitos. 4. **Dados que são SEUS ≠ de um cliente** — recursos criados **sem** `tenant_id` ficam no nível do projeto (compartilhados). Reserve `tenant_id` para dado que pertence de fato a um cliente final. ## Granularidade extra (Vector): por usuário final dentro de um tenant Para isolar **por usuário final dentro de um tenant** sem multiplicar coleções, use o `filter` por metadata na busca do Vector (ex. `"filter":{"end_user_id":"…"}`, AND entre as chaves). O `tenant_id` dá a fronteira **física** (schema dedicado); o `filter` dá a fronteira **lógica** fina dentro dela. Ver [Vector](vector.md). ## Gotchas - **Não crie um projeto por cliente final** — cliente final é Tenant, não projeto. - **Não use `tenant_id` diferente em cada produto** — quebra a ligação transversal. - **Não use o nome comercial mutável como `tenant_id`** — use um id estável. - **No Cache, prefixe sempre** — sem `t::`, chaves de clientes diferentes se misturam. - A estratégia de tenancy do Database (`row_level`/`schema_per_company`) é **imutável por projeto** — escolhida na criação do projeto. ---