People
AlphaSaber quanto você sabe de uma pessoa, e saber quando parou de saber
Todo mundo guarda o CPF do cliente. Quase ninguém sabe dizer se aquele endereço foi conferido, por quem, e se ainda vale. O People guarda a resposta e a mantém honesta sozinho.
- Fintech de crédito que precisa provar, por operação, que conhecia o cliente naquele dia
- Correspondente bancário e marketplace que atendem o mesmo CPF para clientes diferentes
- Operação com revendedor, onde cada subconta precisa ver só as pessoas dela
- Produto regulado que responde pedido de titular sob LGPD sem virar planilha
- Tabela de clientes caseira com colunas de CPF, renda e endereço sem procedência
- Planilha de controle de validade de documento
- Rotina manual de atendimento a pedido de titular sob LGPD
- Cadastro de login ou autenticação do usuário final — isso é o IAM
- Motor de score ou de decisão de crédito — o People não pontua
- Controle antifraude documental
- Registro de pessoa jurídica — isso é o Companies
16 endpoints em 1 recurso.
Resumo executivo
O People guarda o que a sua empresa sabe sobre uma pessoa física — e, junto com cada informação, de onde ela veio, quem conferiu e até quando vale. Não é uma lista de clientes: é o registro do que foi provado sobre cada um, com prazo de validade embutido.
Na prática, ele responde uma pergunta que hoje ninguém responde sem abrir cinco telas: "para
liberar esta operação, eu sei o suficiente sobre esta pessoa?" O comprovante de residência
venceu ontem? O nível cai sozinho, a resposta muda de sim para não e um evento avisa quem
precisa saber — sem ninguém rodar nada.
O núcleo está implementado; o bloco não está completo. status: alpha — existe código em
src/people/, schema people no Postgres, 15 rotas, fachada de dois métodos e 68 testes. O que
ainda NÃO existe, de propósito, está listado em §15: consentimento formal com texto versionado,
merge de duplicata, relatório e expurgo do titular, prova de vida pela porta de biometria e a
ponte com o customers. O núcleo deixa o gancho pronto para os cinco — tabela, coluna ou porta —
sem implementá-los. Nada foi deployado. O desenho continua em openspec/changes/people-block/
e o que existe hoje ao lado dele é o bloco Customers, que segue intacto
(§12).
| Atributo | Valor |
|---|---|
| Identificador | people |
| Categoria | Identidade |
| Escopo | Tenant e subconta |
| Porta (standalone) | 3038 |
| Path alias | @people |
| Status | Alpha — núcleo implementado, 2026-09 |
| Depende de | IAM (duro); File Storage (fachada, opcional); Bureaus, Biometrics e Data Extraction (portas, ainda não ligadas) |
O problema
negócioO cenário. Uma operação de crédito recebe um cliente pelo WhatsApp, pede uns documentos, consulta um bureau, aprova. Seis meses depois, o cliente volta. Ninguém sabe dizer o que foi conferido da primeira vez, nem se ainda vale.
O que trava hoje.
- O dado não tem origem. No
customers,monthlyIncomeé uma coluna. A renda que o cliente digitou e a renda que veio do bureau ocupam o mesmo lugar. Quem lê depois não distingue, e nenhuma política consegue exigir "renda comprovada" sem gambiarra no consumidor. - Nada vence. Um comprovante de 2023 e um de ontem valem igual. A regra que exigia documento recente vira responsabilidade de alguém lembrar.
- O documento fica em claro.
Person.taxIdValueéStringsem cifra e sem coluna de hash; a busca por CPF é igualdade em texto claro. O blocobiometrics, que nasceu depois, já guarda o sujeito por HMAC. - Não há subconta. Um revendedor não consegue separar as pessoas dos clientes dele: o modelo
só tem
organizationId. - Ninguém consome.
customersnão tem fachada e nenhum outro bloco o importa. Cada produto reimplementa "quem é essa pessoa" por dentro.
O custo de não resolver. Toda contestação de operação vira arqueologia: abrir a conversa,
achar o print, torcer para o arquivo ainda estar no lugar. Toda auditoria de LGPD vira trabalho
manual — o data-extraction já documenta o custo dessa lacuna no §15 dele, chamando o expurgo
manual de "a lacuna mais relevante para compliance". E toda regra de negócio que dependia de
"documento recente" passou a depender de disciplina humana, que é a garantia mais cara que existe.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| "O endereço dele é esse" | "O endereço foi conferido em bureau no dia 12/03, vale até 12/09" |
| A ficha não muda quando a evidência vence | O nível cai sozinho na leitura seguinte, e um evento avisa |
| CPF em texto claro na tabela | CPF cifrado em repouso, buscável por hash |
| Cada produto decide o que é "conhecer o cliente" | Uma pergunta só, com o requisito vindo de quem pergunta |
| Revendedor vê tudo da organização | Subconta vê só o que é dela; fora do escopo responde 404 |
Procedência por atributo. Cada informação carrega origem, confiança, quando foi verificada e quando expira — e valores de origens diferentes convivem em vez de se sobrescreverem em silêncio.
Nível que cai sozinho. O nível não é coluna: é calculado na leitura sobre as evidências válidas naquele instante. Entre a expiração e a leitura não existe janela em que a ficha mente.
Uma pergunta, uma resposta. GET /people/:id/assurance?required=evidenced devolve
satisfied, o nível por dimensão e o que falta. Quem pergunta traz o requisito; o bloco não
precisa conhecer o produto.
LGPD, com o gancho pronto. O consentimento tem tabela, o expurgo tem onde olhar, e o relatório tem de onde ler — mas as três rotas são do próximo PR (§15). O que o núcleo já garante é o que torna as três possíveis depois: procedência em toda informação e documento cifrado com leitura em claro sob permissão própria e com evento.
Casos de uso reais
negócioCaso 1 — O cedente que voltou seis meses depois Cenário ilustrativo
Fundo de antecipação de recebíveis médicos, cerca de duas dezenas de cedentes ativos, operações mensais.
O cedente volta para uma nova cessão. O analista não sabe o que foi conferido na primeira vez. Na dúvida, pede tudo de novo — e o cedente, que já mandou aquilo, reclama.
GET /people/:id/assurance?required=evidenced antes de abrir a proposta.
A resposta diz que identidade e renda seguem comprovadas e que só o comprovante de residência
venceu. O fluxo pede um documento.
De "mande tudo de novo" para "falta só isto", sem ninguém conferir manualmente.
Caso 2 — O mesmo CPF em dois clientes do mesmo revendedor Cenário ilustrativo
Plataforma vendida a um revendedor que atende duas financeiras.
As duas financeiras atendem a mesma pessoa. Num modelo com escopo só de organização, ou as duas veem o mesmo registro (vazamento) ou a segunda não consegue cadastrar (colisão).
Escopo de subconta desde o schema, unicidade por dono exato
(organizationId + subaccountId + hash do documento), e 404 — nunca 403 — para quem pede
o registro do vizinho: um 403 confirmaria que o registro existe.
Cada financeira tem a sua ficha, com o seu histórico de evidência, e o revendedor vê as duas.
Caso 3 — A regra que dependia de alguém lembrar Cenário ilustrativo
Esteira de crédito com política que exige comprovante de residência com menos de 90 dias.
A regra estava escrita na política e a verificação era manual. Operação aprovada com comprovante vencido só aparecia na auditoria.
O comprovante entra com expiresAt. No dia 91, a dimensão address cai
na primeira leitura e a varredura diária publica people.assurance.expired — o workflow da
esteira ouve o evento e pede a renovação antes de a operação chegar à mesa.
A regra passa a se aplicar sozinha; a auditoria deixa de ser o primeiro lugar onde o problema aparece.
Caso 4 — O pedido do titular Cenário ilustrativo
Operação regulada que recebe um pedido de acesso sob o art. 18 da LGPD.
Responder exige varrer banco, storage e conversas, à mão, com prazo legal correndo.
POST /people/:id/report devolve tudo — atributos com origem,
documentos, verificações, consentimentos com o texto que foi aceito. POST /people/:id/erase
expurga, preserva a trilha sem o dado pessoal e diz o que não pôde apagar por obrigação legal.
O que era um mutirão vira duas chamadas, com registro do que foi feito.
⚠️ Ainda não. As duas rotas são o próximo PR (§15). Este caso descreve o destino, não o que responde hoje.
Mercado e diferenciais
negócioPanorama. O mercado vende verificação, não registro. Persona, Onfido e Idwall orquestram muito bem o momento do onboarding: consultam, leem documento, batem selfie, devolvem aprovado ou reprovado. O que fazer com aquilo depois — onde guardar, como saber que envelheceu, como provar dali a dois anos o que se sabia no dia — continua sendo problema do cliente. É o espaço em que o People vive: ele não compete com a verificação, ele é o lugar onde o resultado dela mora e envelhece.
| Critério | Catalisa People | Persona | Onfido Studio | Idwall |
|---|---|---|---|---|
| Registro persistente com procedência por atributo | Sim | Parcial | Não | Parcial |
| Nível que decai por vencimento da evidência | Sim | Não | Não | Não |
| Escopo de subconta (revendedor) | Sim | Não | Não | Não |
| Catálogo de provedores de verificação | Pelos blocos, um por tipo | Amplo | Amplo | Amplo (Brasil) |
| Editor visual de esteira | Não (é o Decision Platform) | Sim | Sim | Sim |
| Dado sai no seu formato | Sim | Parcial | Parcial | Parcial |
| Preço | Em definição | Por verificação | Por verificação | Por consulta |
Nossos diferenciais.
- O nível decai sozinho. Não é um campo com job por trás — é derivado na leitura. Copiar exige repensar o modelo de dados inteiro, não acrescentar uma coluna.
- Procedência por atributo. Ter um campo "verificado: sim/não" é fácil; ter cada atributo com origem, confiança e prazo, e a política conseguindo exigir "esta origem, no máximo com tantos dias", muda o que dá para escrever como regra.
- Subconta no schema. Quem nasceu com um nível de tenancy raramente ganha o segundo sem reescrever a camada de acesso.
Quando escolher o concorrente. Se o seu problema é verificar e não guardar — você precisa de leitura de documento em 40 países amanhã, com certificação eIDAS — Onfido ou Persona entregam isso hoje e nós não. Se você quer um editor visual em que o time de risco monta a esteira sem engenharia, Persona é melhor nisso. E se você precisa de cobertura de fontes brasileiras pronta com esteira embutida, a Idwall resolve mais rápido. O People faz sentido quando o registro é seu ativo — quando você vai precisar, dali a dois anos, provar o que sabia naquele dia.
Modelo de cobrança e ROI
negócioPrecificação em definição. O bloco está especificado e nenhum preço foi fechado. O que já está decidido são os drivers.
| Driver | Por que ele | Comportamento esperado |
|---|---|---|
| Pessoas ativas no período | É a unidade que o cliente entende e que cresce com o negócio dele | Cresce devagar, previsível |
| Verificações realizadas | Cada verificação tem custo de fornecedor por trás | Picos no onboarding |
| Documentos lidos | Leitura por modelo de visão tem custo por página | Proporcional ao volume de entrada |
Por que a unidade é justa. Cobrar por consulta pune quem organiza o processo; cobrar por
pessoa ativa alinha o preço ao valor. E consulta cacheada não é cobrada duas vezes — é a mesma
disciplina que o bureaus já aplica, cujo próprio tagline é "cache que impede pagar duas vezes
pelo mesmo CPF".
A conta de guardanapo. O custo evitado não é a verificação (essa você paga de qualquer jeito) — é a operação: o analista que reconfere o que já estava conferido, o mutirão de LGPD, a contestação que vira arqueologia. Quando houver preço, esta seção ganha a comparação em cenário nomeado, com a data da consulta ao preço público de cada análogo.
Arquitetura
flowchart TD
Cliente["Painel / workflow / outro bloco"] -->|HTTP com JWT ou chave| Rotas
OutroBB["Outro building block"] -->|IPeopleFacade| Servicos
subgraph People
Rotas["Rotas (Hono)"] --> Servicos["Serviços"]
Servicos --> Assurance["AssuranceService (calcula na leitura)"]
Servicos --> Repos["Repositórios (Prisma, schema people)"]
Servicos --> Portas["Portas de saída"]
end
Portas -->|IBureausFacade| Bureaus
Portas -->|IBiometricsFacade| Biometrics
Portas -->|IFileStorageFacade| FileStorage
Portas -->|fachada a criar| DataExtraction
Servicos -->|EventPublisher| Stream["Stream iam-events"]
Stream --> AuditTrail["audit-trail"]
Stream --> Webhooks["webhooks-engine"]Três decisões que não são óbvias.
O nível não é coluna. Se fosse, alguém escreveria nela, e a partir daí o nível vira opinião. Ele é derivado a cada leitura das evidências válidas naquele instante. A varredura diária existe só para avisar que caiu, nunca para mudar o estado — entre a expiração e o job não há janela em que a ficha mente.
Toda saída passa por fachada. O bloco nunca fala HTTP com provedor externo e nunca monta o
caminho de outro bloco à mão. O motivo é concreto: o caminho real do data-extraction é
/data-extraction/api/v1/data-extraction/extractions — o nome do módulo aparece duas vezes,
e quem escreve de cabeça erra. Hoje o núcleo usa uma só: IFileStorageFacade, para conferir
que o fileId do documento é desta organização. As portas de bureau, biometria e leitura de
documento ainda não são disparadas por aqui (§15) — e quando forem, entram pelas fachadas, não
por fetch solto. Fachada ausente no processo vira aviso, não queda: o documento é gravado
sem a conferência do arquivo, que é honesto e não perde o dado.
Auditoria não tem fachada, e não precisa. Todo bloco publica no stream Redis iam-events via
EventPublisher.safePublish, e o audit-trail lê esse mesmo stream por consumer group. O que o
People precisa é publicar com organizationId e subaccountId na metadata — e ganhar entrada
própria em RESOURCE_TYPE_MAPPING, senão o mapeador cai no genérico e grava o organizationId
como id do recurso (bug já corrigido para baas.* e biometrics.*).
No modo monolito as fachadas resolvem para os serviços locais; em standalone, para chamadas HTTP
por ModuleClient. O bloco não sabe em qual modo está.
Conceitos e modelo de dados
Glossário.
| Termo | Definição |
|---|---|
| Nível de conhecimento (assurance) | Quanto se sabe da pessoa: declared, verified, evidenced, certified. Calculado, nunca gravado |
| Dimensão | O eixo em que o nível é medido — identity, address, income, liveness |
| Procedência | Origem + confiança + data de verificação + prazo de um atributo |
| Evidência | Documento, verificação ou consentimento que sustenta uma dimensão |
| Dono | O par organização + subconta. Unicidade e visibilidade são do dono exato |
| Tombstone | Ponteiro deixado por um merge, que resolve o id perdido para o vencedor |
Modelo de dados. Sete modelos no schema people. Toda tabela tem organizationId e
subaccountId String?.
O prefixo dos modelos é People, e não Person: o nome Person já é do customers, e nome
de modelo Prisma é global — o schema nem compila com os dois. Os nomes de TABELA são os da spec.
| Modelo | Tabela | Propósito | Campos-chave |
|---|---|---|---|
PeoplePerson | people.people | A pessoa | documentValue (cifrado), documentHash, documentMask, ownerId, externalRef, mergedIntoId |
PeopleAttribute | person_attributes | Um atributo com procedência | key, value, source, confidence, verifiedAt, expiresAt — único por (pessoa, chave, origem) |
PeopleContact | person_contacts | Telefone ou e-mail | kind, value, verifiedAt, verificationId |
PeopleAddress | person_addresses | Endereço com origem | kind, source, verifiedAt, expiresAt |
PeopleDocument | person_documents | Documento e sua leitura | type, fileId, status, reading, validUntil, readingDeadlineAt |
PeopleVerification | person_verifications | Uma verificação, nossa ou externa | origin, provider, outcome, grade, evidenceRef, expiresAt, idempotencyKey |
PeopleConsent | person_consents | Consentimento colhido — gancho, sem rota ainda | purpose, textHash, templateVersion, channel, revokedAt |
ownerId não estava na spec, e precisa estar. A spec pediu
@@unique([organizationId, subaccountId, documentHash]). Esse índice não impede a duplicata
do caso mais comum — o da própria organização —, porque no Postgres NULL não é igual a NULL:
duas linhas com subaccount_id nulo e o mesmo hash convivem sem violar nada. A coluna derivada
owner_id (subaccountId ?? organizationId) resolve, é expressável em Prisma (índice parcial não
é) e mantém o subaccount_id nulável de que o filtro de leitura precisa. Há prova em SQL disso
no corpo do PR.
Máquina de estados do nível.
stateDiagram-v2 [*] --> declared declared --> verified: fonte externa confere o dado verified --> evidenced: documento válido sustenta evidenced --> certified: prova de vida ou assinatura qualificada certified --> evidenced: a prova mais forte vence evidenced --> verified: o documento vence verified --> declared: a verificação vence
As setas para baixo não precisam de comando: acontecem porque a evidência deixou de estar válida no momento da leitura.
Máquina de estados do documento.
stateDiagram-v2 [*] --> pending pending --> uploaded: arquivo confirmado no File Storage uploaded --> reading: leitura pedida ao Data Extraction reading --> read: leitura concluída reading --> pending: prazo estourado, com motivo operacional read --> valid: conferida e dentro da validade valid --> expired: passou de validUntil read --> rejected: conferência recusou
Atenção. reading → pending existe por um motivo medido: o consumidor assíncrono do
data-extraction vem desligado por padrão e não aparece nas pilhas publicadas. A API
responde 202 e a extração fica PENDING para sempre. O People não trata 202 como
sucesso — ele espera, e depois desiste dizendo por quê.
Referência da API
Escrita a partir de
src/people/routes/people.router.ts. São 15 rotas (mais/people/healthe/people/health/ready, montadas pelo helper compartilhado). O que a spec desenhou e este PR não implementou — merge, consentimento, relatório, expurgo,config/*erekey— saiu desta seção e está em §15, para a tabela não prometer rota que não responde.
Prefixo: /people/api/v1. Toda rota exige authMiddleware, requirePermission(...) e contexto
de organização. Todas aceitam chave de subconta e o header X-Subaccount-Id (people está em
SUBACCOUNT_AWARE).
Pessoas
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
| POST | /people | Cria ou reconhece por documento; idempotente por externalRef. 201 na criação, 200 no reconhecimento | PEOPLE_WRITE |
| GET | /people | Lista por cursor (?cursor=&limit=&externalRef=) | PEOPLE_READ |
| GET | /people/:id | Ficha, documento mascarado. ?reveal=true exige PEOPLE_READ_DOCUMENT e publica evento | PEOPLE_READ |
| PATCH | /people/:id | Atualiza nome, externalRef, atributos, contatos e endereços — sempre com procedência | PEOPLE_WRITE |
| GET | /people/:id/attributes | Atributos com origem, confiança, validade e divergência derivada | PEOPLE_READ |
| POST | /people/resolve | Resolve até 100 referências (id, externalRef ou documento) numa chamada; cada item volta { person, error? } | PEOPLE_READ |
| POST | /people/contacts/resolve | O melhor contato de um tipo (PHONE ou EMAIL) de até 100 pessoas cuja POSSE foi provada por fluxo interno (código enviado a ele e confirmado — PeoplePersonService.confirmContactPossession; a rota POST /people/:id/contacts/possession só responde à identidade interna de servidor, para qualquer outro chamador é 404), com o valor ainda igual ao da prova (HMAC). verifiedAt/verificationId gravados por PATCH ou POST /verifications não contam. Principal primeiro; vencido não conta; sem posse provada, contact: null. reveal: true traz o valor em claro, exige PEOPLE_READ_DOCUMENT (só JWT) e publica people.contact.revealed (só id e tipo). É o telefone do código do revínculo do banking — fachada resolveContact. Quem confirma posse hoje: a abertura de conta do banking (o código foi ao telefone informado e conferido; evento people.contact.possession_confirmed, com quem provou e sem o valor). Fora dela, só contato marcado por migração/seed tem posse | PEOPLE_READ |
Nível de conhecimento
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
| GET | /people/:id/assurance | ?required=&dimensions=&cache= — nível por dimensão, satisfied e o que falta | PEOPLE_READ |
| POST | /assurance/batch | Até 100 referências, resposta 207 item a item | PEOPLE_READ |
Evidência
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
| POST | /people/:id/documents | Registra documento (tipo, emissor, validade, fileId) | PEOPLE_WRITE |
| GET | /people/:id/documents | Lista documentos e situação | PEOPLE_READ |
| POST | /people/:id/verifications | Registra verificação — nossa (INTERNAL) ou trazida pelo cliente (EXTERNAL) | PEOPLE_VERIFY |
| GET | /people/:id/verifications | Histórico | PEOPLE_READ |
| POST | /verifications/ingest | Lote de evidência externa, com chave de idempotência por registro; 207 | PEOPLE_VERIFY |
Política do tenant
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
| GET | /config/policy | O teto que evidência externa pode sustentar (externalMaxGrade) e se é o padrão | PEOPLE_READ |
| PUT | /config/policy | Define o teto. Só a organização: subconta recebe 403 SUBACCOUNT_SCOPE_FORBIDDEN | PEOPLE_ADMIN |
Toda verificação que entra pela rota é EXTERNAL. origin só aceita esse valor; mandar
INTERNAL é recusado com 400, não ignorado. Evidência interna nasce de
PeopleVerificationService.recordInternal, que não tem rota — só uma porta do bloco chama.
Referência ambígua é 409. Com chave de organização (sem X-Subaccount-Id), o mesmo
externalRef ou documento pode existir em duas subcontas. O bloco não escolhe uma: resolve,
assurance/batch e verifications/ingest devolvem PEOPLE_AMBIGUOUS_SUBJECT no item, e nada
é gravado. Repita com a subconta.
A rota principal, em detalhe.
GET /people/api/v1/people/{id}/assurance?required=evidenced&dimensions=identity,incomeGET /people/api/v1/people/{id}/assurance?required=evidenced&dimensions=identity,income{
"data": {
"personId": "3f0a…",
"level": "verified",
"satisfied": false,
"required": "evidenced",
"dimensions": {
"identity": { "level": "evidenced", "until": "2027-03-12T00:00:00.000Z",
"sustainedBy": [{ "kind": "document", "id": "d1…", "source": "document" }] },
"income": { "level": "verified", "until": "2026-10-01T00:00:00.000Z",
"sustainedBy": [{ "kind": "verification", "id": "v2…", "source": "external" }] },
"address": { "level": "declared", "until": null, "sustainedBy": [] },
"liveness": { "level": "declared", "until": null, "sustainedBy": [] }
},
"missing": [
{ "dimension": "income", "current": "verified", "need": "evidenced",
"suggest": "anexe um documento de renda válido" }
],
"cached": true,
"computedAt": "2026-09-22T13:04:11.000Z"
}
}{
"data": {
"personId": "3f0a…",
"level": "verified",
"satisfied": false,
"required": "evidenced",
"dimensions": {
"identity": { "level": "evidenced", "until": "2027-03-12T00:00:00.000Z",
"sustainedBy": [{ "kind": "document", "id": "d1…", "source": "document" }] },
"income": { "level": "verified", "until": "2026-10-01T00:00:00.000Z",
"sustainedBy": [{ "kind": "verification", "id": "v2…", "source": "external" }] },
"address": { "level": "declared", "until": null, "sustainedBy": [] },
"liveness": { "level": "declared", "until": null, "sustainedBy": [] }
},
"missing": [
{ "dimension": "income", "current": "verified", "need": "evidenced",
"suggest": "anexe um documento de renda válido" }
],
"cached": true,
"computedAt": "2026-09-22T13:04:11.000Z"
}
}Quais dimensões entram na conta. Com ?dimensions=, as que você pediu. Sem ele, as que
têm algum sinal — e identity sempre tem, porque a pessoa só existe com um documento informado.
A spec não decidiu isto, e o padrão óbvio (as quatro, sempre) transformaria liveness num veto
universal: toda pessoa sem biometria responderia declared e a pergunta perderia a serventia.
Uma dimensão sem sinal continua visível na resposta — ela só não puxa o resultado para baixo
sem ninguém ter pedido. Para exigi-la, peça-a: ?dimensions=identity,liveness.
| Erro | Quando |
|---|---|
400 | required fora do enum, dimensão desconhecida, lote acima de 100 (PEOPLE_BATCH_TOO_LARGE), atributo sem source, origin: INTERNAL, grau externo acima do teto (PEOPLE_EXTERNAL_GRADE_ABOVE_POLICY) |
403 | Permissão insuficiente — ?reveal=true sem PEOPLE_READ_DOCUMENT (ou com chave de API), PUT /config/policy por subconta |
404 | Pessoa inexistente ou fora do escopo do chamador — o 404 é deliberado |
409 | externalRef já é de outra pessoa do dono (PEOPLE_EXTERNAL_REF_CONFLICT); referência ambígua entre subcontas (PEOPLE_AMBIGUOUS_SUBJECT) |
503 | fileId informado e File Storage indisponível (FILE_STORAGE_UNAVAILABLE) — falha fechado, não grava |
207 | Lote: não é erro, é resposta item a item |
Início rápido
Do zero ao primeiro nível calculado. Em ambiente local (bun run dev:people, porta 3038).
O que foi executado, e o que não foi. O fluxo abaixo — criar, perguntar, registrar evidência, perguntar de novo — está coberto por teste de integração com testcontainers (
tests/integration/people/people-core-flow.test.ts, 15 casos, verdes). Oscurldesta seção não foram rodados contra um serviço no ar: este PR não deploya nada. Eles são a tradução literal do que o teste exercita.
1. Autenticar.
TOKEN=$(curl -s -X POST "$IAM/iam/api/v1/users/login" \
-H 'Content-Type: application/json' \
-d '{"email":"…","password":"…"}' | jq -r '.accessToken')TOKEN=$(curl -s -X POST "$IAM/iam/api/v1/users/login" \
-H 'Content-Type: application/json' \
-d '{"email":"…","password":"…"}' | jq -r '.accessToken')A rota é
/users/logine o token vem na raiz (.accessToken), como documentambaas,biometricseaudit-trail. Até 23/09/2026 esta seção dizia/auth/logine.data.accessToken: quem seguia o início rápido parava num404.Se você acabou de criar o usuário pela API, ele não loga ainda.
POST /iam/api/v1/usersdevolvestatus: "pendente", e o login responde401 Invalid credentials— a mesma mensagem de senha errada, então não há como distinguir olhando a resposta. Ative antes:PATCH /iam/api/v1/users/:idcom{"status":"ativo"}.
2. Criar a pessoa. Idempotente por externalRef; o mesmo documento no mesmo dono reconhece
em vez de duplicar.
curl -s -X POST "$BASE/people/api/v1/people" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Maria Souza","document":{"type":"BR_CPF","value":"12345678909"},
"externalRef":"lead-8821",
"attributes":[{"key":"income","value":"5000","source":"declared","confidence":"LOW"}]}'curl -s -X POST "$BASE/people/api/v1/people" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"Maria Souza","document":{"type":"BR_CPF","value":"12345678909"},
"externalRef":"lead-8821",
"attributes":[{"key":"income","value":"5000","source":"declared","confidence":"LOW"}]}'201 na primeira vez, 200 nas seguintes. O documento volta mascarado:
{ "data": { "id": "3f0a…", "externalRef": "lead-8821",
"document": { "type": "BR_CPF", "masked": "***.***.789-09" } } }{ "data": { "id": "3f0a…", "externalRef": "lead-8821",
"document": { "type": "BR_CPF", "masked": "***.***.789-09" } } }3. Perguntar o nível com um requisito.
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
-H "Authorization: Bearer $TOKEN"curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
-H "Authorization: Bearer $TOKEN"satisfied: false, com missing dizendo o que falta.
4. Registrar a evidência — aqui, uma verificação que o cliente trouxe pronta.
curl -s -X POST "$BASE/people/api/v1/people/3f0a…/verifications" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"dimension":"identity","type":"document.match","origin":"EXTERNAL",
"provider":"idwall","outcome":"APPROVED","grade":"VERIFIED",
"occurredAt":"2026-09-20T10:00:00.000Z","expiresAt":"2027-03-20T10:00:00.000Z",
"evidenceRef":"protocolo-8821"}'curl -s -X POST "$BASE/people/api/v1/people/3f0a…/verifications" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"dimension":"identity","type":"document.match","origin":"EXTERNAL",
"provider":"idwall","outcome":"APPROVED","grade":"VERIFIED",
"occurredAt":"2026-09-20T10:00:00.000Z","expiresAt":"2027-03-20T10:00:00.000Z",
"evidenceRef":"protocolo-8821"}'5. Perguntar de novo. identity sai em verified, com until — mas a resposta global ainda
vem level: declared, satisfied: false:
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
-H "Authorization: Bearer $TOKEN" # → level: declared, satisfied: false
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified&dimensions=identity" \
-H "Authorization: Bearer $TOKEN" # → level: verified, satisfied: truecurl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified" \
-H "Authorization: Bearer $TOKEN" # → level: declared, satisfied: false
curl -s "$BASE/people/api/v1/people/3f0a…/assurance?required=verified&dimensions=identity" \
-H "Authorization: Bearer $TOKEN" # → level: verified, satisfied: trueNão é defeito, é a regra de §9 "quais dimensões entram na conta": sem ?dimensions=, entram
todas as que têm algum sinal — e o atributo income do passo 2, ainda que declared, é um
sinal. A dimensão income entra na conta em declared e puxa o nível global para baixo. Pergunte
pela dimensão que o seu ato exige (?dimensions=identity), ou registre evidência de renda também.
Até 23/09/2026 este passo prometia satisfied: true sem o recorte — o smoke bateu nisto.
6. Uma tela inteira, numa chamada.
curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"required":"verified","refs":[{"id":"3f0a…"},{"externalRef":"lead-8821"}]}'curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"required":"verified","refs":[{"id":"3f0a…"},{"externalRef":"lead-8821"}]}'Resposta 207: cada item resolvido por si, um ruim não derruba os outros.
Receitas
Receita 1 — Liberar uma operação só se o nível bastar
Objetivo. Não deixar a operação seguir com conhecimento insuficiente.
RESP=$(curl -s "$BASE/people/api/v1/people/$ID/assurance?required=evidenced" -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq -e '.data.satisfied == true' >/dev/null || { echo "$RESP" | jq '.data.missing'; exit 1; }RESP=$(curl -s "$BASE/people/api/v1/people/$ID/assurance?required=evidenced" -H "Authorization: Bearer $TOKEN")
echo "$RESP" | jq -e '.data.satisfied == true' >/dev/null || { echo "$RESP" | jq '.data.missing'; exit 1; }Armadilha. O required vem de quem pergunta, nunca do bloco. Qual nível cada ato exige é
regra do produto e vive na política da vertical — o People não conhece o ato.
Receita 2 — Uma tela com 80 pessoas, sem 80 chamadas
Objetivo. Listar a fila da mesa sem derrubar a tela.
curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"required":"verified","refs":[{"id":"…"},{"externalRef":"lead-8821"}]}'curl -s -X POST "$BASE/people/api/v1/assurance/batch" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"required":"verified","refs":[{"id":"…"},{"externalRef":"lead-8821"}]}'Resposta 207: cada item resolvido por si, um ruim não derruba os outros.
Armadilha. Não substitua isto por um laço no cliente. Uma chamada por linha de tela já nos
custou uma tela de 38 s, uma resposta de 16 MB e recusa por rajada — e nenhuma das três apareceu
em teste, só no cliente. Teto de 100 por lote, e paginação por cursor, nunca por offset.
Receita 3 — Renovar antes de a evidência vencer
Objetivo. Pedir o documento novo antes de a operação travar.
Assine people.assurance.expired no webhooks-engine e dispare o fluxo de renovação. O evento
traz pessoa, dimensão, nível anterior e nível atual.
Armadilha. O evento é aviso, não causa. O nível já tinha caído na leitura anterior; quem
depende de estado lê o assurance, não o evento.
Receita 4 — Ingerir evidência que o cliente já tem
Objetivo. Aproveitar o que já foi verificado fora, sem fingir que fomos nós.
curl -s -X POST "$BASE/people/api/v1/verifications/ingest" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"items":[{"person":{"externalRef":"lead-8821"},"dimension":"income",
"type":"income.statement","origin":"EXTERNAL","provider":"parceiro",
"outcome":"APPROVED","grade":"VERIFIED","occurredAt":"2026-09-20T10:00:00.000Z",
"idempotencyKey":"lote-7/linha-1"}]}'curl -s -X POST "$BASE/people/api/v1/verifications/ingest" \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"items":[{"person":{"externalRef":"lead-8821"},"dimension":"income",
"type":"income.statement","origin":"EXTERNAL","provider":"parceiro",
"outcome":"APPROVED","grade":"VERIFIED","occurredAt":"2026-09-20T10:00:00.000Z",
"idempotencyKey":"lote-7/linha-1"}]}'Armadilhas. A chave de idempotência é por registro e é do dono: rodar o lote de novo
devolve 200 duplicate em vez de criar a segunda linha — é isso que faz a retomada de uma carga
interrompida ser segura. Gere a chave com o id da pessoa dentro; a mesma chave apontando para
outra pessoa é recusada (PEOPLE_IDEMPOTENCY_CONFLICT). E origin: EXTERNAL exige provider e
occurredAt: sem quem verificou e quando, não é evidência, é afirmação — e ela fica marcada como
externa para sempre, porque não somos nós que podemos auditá-la.
Receita 5 — Saber de onde veio cada informação
Objetivo. Responder "esse endereço foi conferido por quem, e ainda vale?".
curl -s "$BASE/people/api/v1/people/$ID/attributes" -H "Authorization: Bearer $TOKEN" | jq '.data[]'curl -s "$BASE/people/api/v1/people/$ID/attributes" -H "Authorization: Bearer $TOKEN" | jq '.data[]'Cada atributo volta com o vigente (o de maior confiança entre os não vencidos), as others
origens e divergent: true quando duas origens válidas afirmam valores diferentes.
Armadilha. divergent é derivado na leitura, como o nível — não há coluna para marcá-lo,
e não há escrita que "resolva" a divergência. Resolver é acrescentar uma origem mais forte.
Merge de duplicata, relatório e expurgo do titular (
/merge,/report,/erase) são do próximo PR — ver §15. Até lá, não existe rota para eles, e esta seção não finge que existe.
Integração com outros building blocks
| BB | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token; organização, subconta e permissões | Sim |
| File Storage | Guarda o arquivo do documento; o People guarda o fileId e o confere pela fachada | Ligado |
| Bureaus | Porta de verificação por consulta externa, pela IBureausFacade | Não ligado (§15) |
| Biometrics | Prova de vida; a evidência assinada sustenta liveness | Não ligado (§15) |
| Data Extraction | Lê o documento enviado — ver a ressalva abaixo | Não ligado (§15) |
| E-Signature | Assinatura que pode sustentar certified | Não ligado (§15) |
| Document Template | Texto versionado do consentimento | Não ligado (§15) |
| Audit Trail | Lê os eventos do stream e deriva a trilha | Automático |
| Webhooks Engine | Entrega people.* ao cliente | Opcional |
| Companies | Aponta para pessoas como sócio, beneficiário ou representante | — |
| Customers | Antecessor; convivência em cinco passos | — |
flowchart LR Captura["Captura (WhatsApp, portal)"] --> People People -->|consulta| Bureaus People -->|prova de vida| Biometrics People -->|lê documento| DataExtraction["Data Extraction"] People -->|guarda arquivo| FileStorage["File Storage"] People -->|nível basta?| Esteira["Esteira de crédito"] Companies -->|ponteiro de sócio| People People -->|eventos| Auditoria["audit-trail + webhooks-engine"]
Atenção. O consumidor assíncrono do data-extraction vem desligado por padrão
(EXTRACTION_CONSUMER_ENABLED=false) e não aparece nas pilhas publicadas — a API responde
202 e a extração fica PENDING para sempre. Ligar esse consumidor é pré-requisito de
qualquer fluxo do People que dependa de leitura de documento, e o instalador da vertical
declara a dependência.
Convivência com o Customers, em cinco passos: fachada de compatibilidade → espelho de escrita
→ migração por organização com relatório de duplicata → virada de leitura → customers marcado
legado. Nada é apagado no mesmo passo em que é migrado. Detalhe em
openspec/changes/people-block/design.md.
Configuração e operação
Variáveis de ambiente.
| Nome | Descrição | Obrigatória | Padrão |
|---|---|---|---|
PEOPLE_ENCRYPTION_KEY | Chave de 32 bytes em hex para cifrar o documento em repouso | Sim | — |
PEOPLE_DOCUMENT_HMAC_KEY | Chave do HMAC de busca por documento | Sim | — |
PEOPLE_ASSURANCE_CACHE_TTL | TTL do cache do nível, em segundos | Não | 60 |
PEOPLE_BATCH_MAX | Teto de itens por lote | Não | 100 |
PEOPLE_OUTBOUND_CONCURRENCY | Teto de chamadas simultâneas às portas, por organização | Não | 8 |
PEOPLE_DOCUMENT_READING_TIMEOUT_MINUTES | Quanto um documento espera em READING antes de o bloco desistir | Não | 30 |
PEOPLE_EXPIRY_SCAN_ENABLED | Liga a varredura de vencimento (ela só avisa) | Não | true |
MODULE_PEOPLE_URL / MODULE_PEOPLE_PORT | Descoberta do módulo em standalone | Só em standalone | `` / 3038 |
Atenção. Definir PEOPLE_ENCRYPTION_KEY ou PEOPLE_DOCUMENT_HMAC_KEY depois de já haver
dado gravado torna o dado ilegível. A troca exige rewrap — é o que POST /rekey faz, e é
a mesma dívida que src/shared/lib/credential-crypto.ts já registra para as credenciais.
Infraestrutura. PostgreSQL (schema people), Redis (cache do nível e stream iam-events),
S3/MinIO pelo File Storage.
Limites. Lote de 100 (/assurance/batch e /verifications/ingest); /people/resolve também
em 100; concorrência interna de 8; listagem só por cursor, no máximo 100 por página.
O cache do nível tem DOIS tetos, não um. O TTL configurado e o instante da próxima
expiração, o que vier antes. Só o TTL não bastaria: uma evidência que vence em 5 segundos com TTL
de 60 serviria 55 segundos de resposta errada — exatamente a janela que o bloco existe para não
ter. A chave guarda a ENTRADA do cálculo, não a resposta, então ela serve a qualquer required;
e toda escrita de evidência a invalida. A chave é (organização, pessoa) — não leva o dono de
quem lê: a primeira versão levava, e a organização gravando evidência deixava a chave da subconta
mentindo por até 60 s. Quem pode ler é decidido antes de o cache entrar.
Catálogo de erros.
| Código | Significa | O que fazer |
|---|---|---|
VALIDATION (source ausente) | Atributo sem procedência | Informe source; não há padrão |
PEOPLE_BATCH_TOO_LARGE | Lote acima do teto | Quebre em páginas; não faça laço por linha |
PEOPLE_EXTERNAL_REF_CONFLICT (409) | externalRef já é de outra pessoa deste dono | Corrija pelo PATCH, não pelo POST |
PEOPLE_AMBIGUOUS_SUBJECT (409) | Referência casa com pessoas de mais de uma subconta | Repita com X-Subaccount-Id; o bloco nunca escolhe |
PEOPLE_EXTERNAL_GRADE_ABOVE_POLICY (400) | Evidência externa acima do teto do tenant | Baixe o grade ou mude a política com PEOPLE_ADMIN |
PEOPLE_IDEMPOTENCY_CONFLICT (400) | Chave de idempotência já usada para OUTRA pessoa | Gere a chave com o id da pessoa dentro |
PEOPLE_FILE_NOT_FOUND (400) | fileId não é desta organização | Confira o arquivo no File Storage |
FILE_STORAGE_UNAVAILABLE (503) | fileId informado e a fachada não está no processo | Suba o File Storage ou anexe sem fileId |
SUBACCOUNT_SCOPE_UNSUPPORTED | O bloco não está em SUBACCOUNT_AWARE | Erro de configuração do deploy |
Eventos publicados (stream iam-events, com organizationId e subaccountId na metadata;
people.* tem entrada própria em RESOURCE_TYPE_MAPPING do audit-trail):
people.person.created, people.person.updated, people.person.revealed,
people.document.attached, people.verification.completed, people.assurance.raised,
people.assurance.expired. Nenhum carrega dado pessoal — nem documento, nem documento
mascarado, nem nome, nem contato: só id, dimensão, nível, origem e referência da prova. Há teste
que varre os eventos publicados procurando os CPFs usados no fluxo.
Observabilidade. O que existe hoje é o log estruturado de cada varredura e de cada falha de cache. Métrica de distribuição de nível por dimensão e histograma das portas de saída ainda não existem — estão em §15, não aqui.
Instalação por vertical. Tudo que é configurável por tenant — tipos de documento e validade,
finalidades de consentimento, portas ligadas, retenção, mapa de dimensões — é declarado pelo
pacote da vertical e semeado pelo instalador com a chave do tenant, pelo mesmo caminho que
creditBlocks já usa. Um tenant novo não nasce com o bloco vazio. O que fica de fora, de
propósito: credencial de provedor, chave de cifra e texto legal próprio do cliente — segredo e
jurídico não entram em template versionado. Três regras herdadas de instalações que quebraram:
o pacote declara tudo o que usa (pacote que usa o não declarado instala com 201 e o tenant
nasce quebrado sem aviso); a instalação é retomável e idempotente por item, com relatório do
que faltou; e o conteúdo vem do banco, então a ordem é publicar, semear e só então instalar.
Detalhe em openspec/changes/people-block/design.md.
Segurança e compliance
Isolamento. Duas camadas. O middleware compartilhado resolve organização e subconta e falha
fechado: um bloco fora do mapa SUBACCOUNT_AWARE recusa chave de subconta e recusa o header,
em vez de ignorá-los — por isso people entra no mapa no mesmo commit que cria o bloco. Dentro
do bloco, leitura e posse usam predicados distintos: a organização enxerga tudo que é dela; a
subconta enxerga só o dela; e a checagem de unicidade usa o dono exato. Recurso fora do
escopo responde 404, nunca 403 — um 403 confirmaria a existência do recurso alheio.
Dado sensível. O documento é cifrado em repouso com aes-256-gcm pelo núcleo compartilhado
de cifra, e a busca usa HMAC-SHA-256 — o hash resolve unicidade e busca sem ser reversível, e
leva a organização dentro da entrada, então o mesmo documento em tenants diferentes não
produz o mesmo hash. O número do documento anexado (person_documents.number) é cifrado pela
mesma chave: RG e CNH são dado pessoal igual.
Ver o documento inteiro é um ato, não uma consulta. PEOPLE_READ devolve a máscara
(***.***.789-09). O valor em claro exige ?reveal=true e a permissão separada
PEOPLE_READ_DOCUMENT, e a leitura publica people.person.revealed. Isto é uma correção ao
desenho: a spec dizia que o documento em claro aparecia "a quem tem PEOPLE_READ", o que
tornaria a máscara decorativa — quem lê a ficha leria o CPF de qualquer jeito.
Chave de API nunca revela. A chave autoriza por padrão de recurso (allowedResources), não
por RBAC; o middleware a normaliza num user com permissions: []. Portanto ?reveal=true com
chave de API é sempre 403, e é deliberado: automação não precisa do CPF em claro, e uma chave
vazada não consegue exportar a base. Só JWT de usuário com PEOPLE_READ_DOCUMENT vê o valor.
O que é IMPOSTO sobre a evidência, e onde.
| Regra | Onde | O que acontece se violar |
|---|---|---|
Pela rota, origin é sempre EXTERNAL | Zod da rota (origin: literal('EXTERNAL')) e o serviço público, que nem lê origin do input | 400 na rota; no serviço, grava EXTERNAL mesmo que o schema fosse burlado |
INTERNAL só nasce de porta do bloco | PeopleVerificationService.recordInternal, sem rota | Não há como chegar por HTTP |
| Evidência externa obedece ao teto do tenant | PeoplePolicyService.externalMaxGrade no caminho de gravação | 400 PEOPLE_EXTERNAL_GRADE_ABOVE_POLICY; nunca rebaixada em silêncio |
O teto padrão é CERTIFIED (externa vale igual à nossa) | Decisão do dono; o tenant aperta com PUT /config/policy | — |
| Política é da organização | PUT /config/policy recusa subconta | 403 SUBACCOUNT_SCOPE_FORBIDDEN |
Evidência externa exige provider e occurredAt | Zod da rota | 400 |
Nada de dado pessoal em log, evento ou mensagem de erro. Os eventos carregam id, dimensão, nível e referência da prova; o erro de item de lote é genérico de propósito, porque erro de banco carrega valor de coluna e coluna aqui é dado pessoal. Retenção configurável de evidência por tenant ainda não existe (§15).
LGPD. Base legal e finalidade ficam no consentimento, com o hash do texto exibido e a versão do modelo — é o que permite provar depois qual texto a pessoa aceitou; a tabela existe e a rota é o próximo PR (§15). Relatório do titular (art. 18) e expurgo ainda não existem como rota — e o README diz isso em vez de repetir o plano. O que o núcleo já entrega para eles é o que os torna possíveis: cada informação com origem e prazo, e o documento cifrado.
Autenticação e permissões. JWT do IAM ou chave de API. Cinco permissões, cada uma com
rota: PEOPLE_READ (ficha, nível, listas), PEOPLE_WRITE (pessoa, documento), PEOPLE_VERIFY
(verificação e ingestão), PEOPLE_ADMIN (PUT /config/policy) e PEOPLE_READ_DOCUMENT
(?reveal=true) — esta última porque ver o documento inteiro não pode vir junto com ler a ficha.
E elas estão atribuídas a papéis no catálogo padrão (src/iam/constants/permissions.ts), não
só declaradas: VIEWER ganha PEOPLE_READ (e não PEOPLE_READ_DOCUMENT — quem só observa
não precisa do CPF); CUSTOMER_MANAGER ganha as outras quatro, inclusive PEOPLE_ADMIN;
VERIFICATION_OPERATOR ganha PEOPLE_READ e PEOPLE_VERIFY; ADMIN já recebe tudo. O data-extraction é o
contra-exemplo que motivou isso: as permissões dele "não estão atribuídas a nenhum papel
intermediário no catálogo padrão do IAM".
Limitações conhecidas
| Limitação | Detalhe | Gravidade |
|---|---|---|
| Consentimento é tabela, não rota | person_consents existe como gancho; nenhuma rota escreve nela. Consentimento formal com texto versionado é o próximo PR | Próximo passo declarado |
| Sem merge de duplicata | A coluna merged_into_id existe como gancho; POST /merge e o tombstone não | Próximo passo declarado |
| Sem relatório e sem expurgo do titular | POST /report e POST /erase são do próximo PR. Até lá, atender art. 18 aqui continua manual | Próximo passo declarado |
Sem POST /rekey | Trocar chave exige recifrar a base, e a rota que faz isso ainda não existe. Definir as chaves antes do primeiro registro | Operação |
config/* por tenant tem um botão só | config/policy (teto da evidência externa) existe. Tipos de documento, finalidades, portas e mapa de dimensões ainda são vocabulário de código. O instalador da vertical depende disto | Próximo passo declarado |
| Portas de verificação não ligadas | Bureau, biometria e leitura de documento ainda não são disparados pelo bloco; recordInternal é o gancho por onde entram. O que funciona hoje é ingestão de evidência externa: o cliente traz o resultado no nosso vocabulário | Por desenho, neste PR |
| Nada deployado | Stacks e docker-compose declaram o serviço; nenhum ambiente subiu | Declarado |
| A leitura de documento depende de um consumidor desligado | EXTRACTION_CONSUMER_ENABLED é false por padrão e não aparece nas pilhas publicadas; a API responde 202 e a extração fica PENDING para sempre | Bloqueante para fluxo com documento |
| Um provedor de visão | Só o Google está implementado no data-extraction; OpenAI e Anthropic recusam com "not yet implemented" | Não anuncie multi-provedor |
| Sem antifraude documental | O data-extraction "lê o que está escrito e acredita"; documento adulterado é lido normalmente | Por desenho — não use como controle antifraude |
| Três fachadas não existem | data-extraction, e-signature e document-template não têm fachada hoje; precisam ser criadas antes das portas | Trabalho declarado |
| Troca de chave exige rewrap | Trocar PEOPLE_ENCRYPTION_KEY sem migrar torna o dado ilegível | Operação, com POST /rekey |
certified depende de assinatura qualificada | O e-signature entrega SIMPLE com o provedor aberto; certified exige provedor que entregue nível superior | Depende de contratação |
| Sem grafo de relacionamento entre pessoas | person.related_people existe no bureaus e o People guarda o resultado como atributo, sem navegação | Roadmap |
| Sem métrica de negócio | Distribuição de nível por dimensão e histograma de porta não são emitidos | Roadmap |
| Preço não definido | Só os drivers estão decididos | Comercial |
Pessoa é do tenant, e isso é decisão de produto — não limitação técnica. O mesmo CPF em duas
organizações são dois registros distintos, com histórico, consentimento e nível de
conhecimento próprios. Não existe pessoa global, nem tabela comum, nem deduplicação entre
organizações, nem reaproveitamento de verificação de um tenant por outro — nem quando isso
economizaria uma consulta paga a bureau. O motivo é que a base legal do tratamento é de cada
tenant: o consentimento que a organização A colheu não autoriza a organização B a saber nada, e
a vantagem comercial de uma base compartilhada não paga o risco jurídico de tratá-la como se
autorizasse. A decisão está no mecanismo, não só na regra de consulta: o document_hash é HMAC
com a organização dentro da entrada, então o mesmo CPF produz hashes diferentes em tenants
diferentes, não há índice sobre document_hash sozinho, e toda consulta entra pelo dono.
Correlacionar as duas fichas é impossível mesmo para quem lê o banco direto. Dois testes de
integração guardam isso: um provando que as duas organizações cadastram o mesmo CPF
independentemente, outro provando que nenhuma rota de uma alcança a pessoa da outra — por id, por
documento ou pela resolução em lote da fachada.
Perguntas frequentes
Por que um bloco novo em vez de melhorar o Customers?
Porque a mudança é estrutural: cifrar o documento, criar coluna de hash, mudar a unicidade e
partir cinco colunas largas numa tabela de atributos com procedência é reescrever o bloco por
dentro, com dado em produção e sem lugar para pousar o intermediário. Como customers não tem
nenhum consumidor em processo hoje, o bloco novo sai barato e a convivência é curta.
O Customers vai ser apagado?
Não no mesmo passo. A sequência é fachada de compatibilidade, espelho de escrita, migração por
organização com relatório de duplicata, virada de leitura e, por último, customers marcado
legado e congelado. A remoção, se acontecer, é um PR separado.
O nível pode ser forçado no braço? Não. Ele é calculado na leitura. O que dá para fazer é acrescentar evidência — e aí ele sobe sozinho. Essa recusa é o ponto do bloco: nível editável é nível que vira opinião.
Como o nível cai sem ninguém rodar nada?
Porque nada foi gravado. A evidência tem expiresAt; no instante em que você lê, o cálculo
ignora o que já venceu. A varredura só avisa com o evento people.assurance.expired. Há teste
que prova isso do jeito mais direto possível: a mesma entrada, calculada em dois instantes, dá
evidenced antes da expiração e declared depois — sem nenhuma escrita entre as duas leituras.
O que já responde e o que ainda não?
Respondem: criar/reconhecer, ficha com procedência, documento, verificação (nossa ou trazida de
fora), ingestão em lote, nível por dimensão, lote de nível e a fachada de dois métodos. Não
respondem ainda: consentimento, merge, relatório e expurgo do titular, config/* por tenant,
rekey e o disparo das portas de bureau/biometria/extração. A lista completa está em §15.
O mesmo CPF pode estar em dois clientes meus? Pode, e deve. A unicidade é do dono exato — organização mais subconta. Duas financeiras do mesmo revendedor atendem a mesma pessoa sem enxergar uma à outra. E quem pede o registro do vizinho recebe 404, não 403.
Quem decide qual nível cada operação exige?
Você, na política da sua vertical. O bloco recebe ?required= e responde se atende e o que
falta. Ele não conhece o seu produto, de propósito.
Isso substitui a Idwall ou a Unico? Não. Elas verificam; o People guarda o resultado com procedência e prazo. Você continua contratando quem verifica — e passa a ter onde o resultado mora e envelhece.
O que acontece se o provedor de leitura de documento estiver fora?
O documento fica em reading até o prazo e depois volta para pending com o motivo operacional.
O que o bloco não faz é tratar o 202 como sucesso — foi exatamente assim que extrações
ficaram PENDING para sempre em outro bloco.
Um tenant novo precisa de configuração manual? Não. Tipos de documento, finalidades de consentimento, portas ligadas, retenção e mapa de dimensões vêm declarados no pacote da vertical e são semeados pelo instalador com a chave do tenant. O que continua manual, de propósito, é credencial de provedor, chave de cifra e texto jurídico próprio.