Skip to content

Modelos de consumo — Taxonomy

consumption specs/consumption/taxonomy.kmd

Define **como um cliente/org consome um componente Koder** como um modelo de dois eixos — **quem opera** (koder × client) × **quem consome** (system × end-user) — e não como uma enumeração de "formas" nomeadas. Cada cruzamento é uma **célula**; cada componente **declara** em que células é oferecido (`koder.toml [consumption]`), e o agregado vive em `registries/consumption-cells.kmd`. Codifica o teste **eixo × atributo**: marca e domínio são **atributos** de deployment (config), não eixos — logo não geram célula nova. Irmão de `specs/variants/taxonomy.kmd`, que faz o mesmo pelo eixo de distribuição.

When this spec applies

Primary triggers

All triggers

Specification body

Modelos de consumo na Koder Stack

1. Definição

Um modelo de consumo é a combinação de duas escolhas sobre um componente Koder entregue a um cliente/org:

  1. Operator — quem opera a instância: koder ou client.
  2. Consumer — quem consome a instância: system (outro software, via API) ou end-user (uma pessoa, via UI).

O cruzamento é uma célula. Uma célula é o que o cliente contrata. Não é a variante (isso é surface × target × form factorspecs/variants/taxonomy.kmd), nem a release (eixo temporal), nem o artefato (build output).

Esta spec não decide quais células a Koder oferece. Ela define o vocabulário e obriga a declaração. Quais células um componente oferece — e por que retém as outras — é dado, declarado no koder.toml do componente (§5) e agregado em registries/consumption-cells.kmd.

2. Por que dois eixos e não uma lista de formas

A pergunta "quais são as formas de usar o Iris?" tinha, em 2026-07-16, quatro respostas plausíveis dadas de memória — e nenhuma lista fechava. O motivo é estrutural: as "formas" não eram uma taxonomia, eram amostras de um espaço. Enumerá-las cai no anti-padrão que a architecture-quality.kmd D3 nomeia ("switch gigante que cresce a cada caso novo"): cada forma lembrada vira um item novo, e o conjunto nunca é provado completo.

Com os eixos, o espaço é fechado por construção (2 × 2 = 4 células), o caso novo cai numa célula em vez de virar item, e as células vazias ficam visíveis em vez de esquecidas.

3. O teste eixo × atributo (normativo)

Regra R1. Uma propriedade só é eixo se ela muda o artefato ou o contrato. Se ela é o valor de um campo de config, é atributo — e atributo nunca gera célula nova.

O teste é herdado de specs/variants/taxonomy.kmd §3.4, que o aplicou pra recusar render strategy como 4º eixo ("não muda qual instância o usuário roda"), citando Solidez Arquitetural D9 (porta de mão dupla): identidade é caminho de mão única; atributo é config, reversível.

3.1 — São eixos

EixoValoresPor que passa em R1
operatorkoder · clientMuda o artefato: o bundle self-hosted da stack-RFC-027 (kpkg/OCI, binários stripped, gate offline, secrets injetados no install, licença offline assinada) não é o serviço hospedado. Muda também custódia do dado, caminho de update e SLA.
consumersystem · end-userMuda o contrato de auth: identidade de sistema (API key) ≠ identidade de pessoa (Koder ID · OIDC, tenant derivado do claim). Determina também se precisa existir variante de UI (web, mobile, …).

3.2 — São atributos (NÃO eixos)

AtributoOnde vivePor que falha em R1
Marca (nome + logo exibidos)config de deploymentO nome canônico do componente não muda. Precedente ratificado: "the component's canonical name stays Koder Iris… 'Vivver Iris' is a per-deployment presentation layer. One codebase, N brands, zero forks" (imaging#068). É um campo (IRIS_BRAND_NAME, PUT /v1/integration/branding).
Domínio (hostname dos endpoints)vhost (infra/net/jet/sites.toml)Hostname é config de borda. Uma variante é a mesma variante em qualquer domínio.

Corolário R1.1 — "white-label" não é um modelo de consumo. Um deployment sob a marca e o domínio do cliente é a mesma célula com dois atributos setados. Não abrir célula, RFC ou modo de produto pra isso. Tornar um atributo configurável num escopo novo (ex.: ACME pra domínio que a Koder não possui) é trabalho de implementação legítimo — mas é trabalho no atributo, não um modelo novo.

4. As células

Id canônico da célula: <operator>-<consumer>.

CélulaOperatorConsumerO que éAuth
koder-systemkodersystemA Koder hospeda; o sistema do cliente chama a API. O modelo de plataforma da stack-RFC-029 (OpenAPI → SDK gerado → API key).API key (identidade de sistema)
koder-end-userkoderend-userA Koder hospeda; a pessoa usa a UI e loga com a conta dela no Koder ID. SaaS multi-tenant.Koder ID / OIDC (tenant do claim)
client-systemclientsystemO cliente opera o bundle stack-RFC-027; o sistema hospedeiro dele integra via SDK.API key (identidade de sistema)
client-end-userclientend-userO cliente opera o bundle; as pessoas dele logam direto na UI.Koder ID / OIDC, ou IdP do cliente

As quatro células são todas significativas — nenhuma é combinação impossível. Célula não-oferecida por um componente é decisão declarada (§5), nunca ausência silenciosa.

Nota — a fronteira do eixo consumer já existe em código. No Koder Iris, backend/internal/authn/authn.go verifica o JWT do Koder ID e deriva o tenant do claim (o caminho end-user), e seu skip() pula /v1/integration explicitamente "[it has] its own authentication" — que é o caminho system (internal/integration, StaticKeyValidator, uma key por deployment). Os dois modelos de auth são o eixo consumer materializado. Esta spec nomeia um contrato que já estava implementado, não inventa um.

5. Declaração por componente (normativo)

Regra R2. Todo componente entregue a cliente/org declara um bloco [consumption] no seu koder.toml. Célula não-oferecida exige motivo nomeado — "não oferecido" sem motivo é drift, não decisão. Desde a stack-RFC-039 o ônus é assimétrico: a célula canônica é koder-*, então é não oferecê-la que exige motivo; oferecê-la não exige justificativa.

Shape (espelha [self_hosted] de policies/self-hosted-first.kmd: declaração no componente + agregado em registry):

# consumption model — see meta/docs/stack/specs/consumption/taxonomy.kmd
[consumption]
# Cells this component is offered in. Vocabulary: <operator>-<consumer>.
offered  = ["client-system"]
# Cells deliberately NOT offered + the named reason. Never silent.
withheld = [
    "koder-system   = custody-gates",
    "koder-end-user = custody-gates",
    "client-end-user = no-ui-variant",
]
# O que o componente processa NÃO se declara aqui — vive no bloco [privacy]
# (specs/privacy/posture-schema.kmd R1 data_collected). Os gates do §6 leem de lá.
# Gates de custódia (§6), quando o componente processa dado sensível:
gates_passed  = ["G2"]
gates_pending = ["G1", "G3", "G4", "G5"]
# Attributes (§3.2) — configurable per deployment; do NOT open a new cell.
brand_configurable  = true
domain_configurable = true

Motivos válidos em withheld (vocabulário fechado):

MotivoSignifica
custody-gatesGated pelo §6 — o componente processa dado sensível e ainda não fechou os gates de custódia. O withheld DEVE vir acompanhado de gates_pending.
owner-decisionDecisão comercial pendente do owner.
no-ui-variantA célula exige uma variante de UI que não existe (specs/variants/taxonomy.kmd).
no-export-bundleA célula exige o bundle stack-RFC-027, ainda não produzido.
not-applicableO componente não faz sentido nessa célula (ex.: engine embarcável).

6. Gate de custódia de dado sensível (normativo)

Regra R3. Uma célula koder-* de componente cujo [privacy].data_collected inclua categoria sensível (health, biometric, financial) abre quando os 5 gates conjuntivos da stack-RFC-039 §4 fecham — não por decisão binária. Enquanto faltar gate: withheld = "<célula> = custody-gates" + os gates_pending correspondentes.

Os gates (G1 postura declarada · G2 régua jurídica enforçada em código · G3 direitos do titular ponta a ponta · G4 detecção e resposta · G5 provado sob a postura) são definidos na rfcs/stack-RFC-039. A forma é a do policies/self-hosted-first.kmd: conjuntivos, avaliados por caso-de-uso (componente × célula, nunca globalmente), apertáveis no koder.toml mas não afrouxáveis sem emenda à RFC.

Por que gate e não decisão. A célula koder-* põe a Koder no data path. Pra um renderizador de mapas isso é trivial; pra um PACS é guarda médica legal — a policies/clinical-imaging-retention.kmd fixa o piso brasileiro em 20 anos (CFM Res. 1.821/2007). Um "sim" de owner contrairia essa obrigação de passagem, e um "não" a adiaria sem condição de saída. O gate converte a pergunta "a Koder custodia dado sensível?" — que não tem resposta estável — em "este componente fechou G1..G5 pra esta célula?", que tem resposta medível e caminho de saída nomeado (Solidez Arquitetural D12: causa-raiz, não sintoma).

G4 é fail-closed — gate não-provado é gate não-passado. O incidente de 2026-07-11 é a evidência de que presumir verde é como o vazamento acontece: o JET-231 flipou 172 vhosts pra auth_gate escopando por domínio e excluiu os não-.koder.dev; o único vhost que servia dado de paciente real (iris.macae-rj.tst.vivver.com, restore do banco do município) estava no domínio do cliente — e foi exatamente o que o rollout pulou: ~8 dias de PHI na internet sem auth. A lição que o notice registra é o teste R1 desta spec com outras palavras: "Rollout de segurança escopado por DOMÍNIO erra justamente as superfícies que mais importam. Escope por o que o vhost serve, não por onde ele mora." Domínio é atributo; o que a superfície serve ([privacy] + célula) é identidade. Escopar por atributo é o bug.

7. Consequências pro host público

Uma célula koder-* precisa de host público; qual host é decidido pela stack-RFC-029 §Q1 — fechada como host por produto via Koder Gate. As duas células koder-* do mesmo componente compartilham o host do produto: koder-end-user serve a UI, koder-system serve a API sob ele. Isso espelha o padrão de URL das landings e mantém o atributo "domínio do cliente" (§3.2) como config de vhost em vez de um sistema à parte.

8. Anti-padrões

  • ❌ "Existe uma forma white-label do produto X" → ✅ "X é oferecido em koder-system; marca e domínio são atributos do deployment" (R1.1).
  • ❌ Abrir RFC/modo de produto pra uma combinação de atributos.
  • ❌ Componente entregue a cliente sem bloco [consumption] (R2).
  • withheld sem motivo nomeado — "não oferecemos" calado (R2).
  • ❌ Escopar rollout de auth/segurança por domínio ou por environment em vez de por o que a superfície serve (§6, incidente 2026-07-11).
  • ❌ Oferecer célula koder-* de componente com dado sensível em [privacy].data_collected sem os gates de custódia fechados (R3).
  • ❌ Declarar o que o componente processa fora do bloco [privacy] — um lugar só (specs/privacy/posture-schema.kmd R1).
  • ❌ Presumir um gate verde por falta de evidência — G4 é fail-closed.
  • ❌ Inventar um 3º eixo sem passar em R1 e sem RFC (análogo a specs/variants/taxonomy.kmd §4).

9. Relação com as RFCs existentes

Esta spec é o guarda-chuva que faltava; ela não revoga nenhuma delas:

RFCPapel sob o modelo
stack-RFC-029Define como a célula koder-system funciona (API por componente, OpenAPI → SDK, front door). Q1 fechada aqui em §7.
stack-RFC-027Define o artefato que faz operator = client existir (o bundle portátil).
stack-RFC-031Define como a entrega chega ao cliente (patch queue / /k-deliver) — ortogonal aos eixos.
stack-RFC-039Decide o default: a célula canônica é koder-*, e define os 5 gates de custódia que o §6 aplica. Esta spec é o vocabulário; aquela é a decisão (RFC-009).
stack-RFC-032Define a hierarquia de tiers de integração (padrão de domínio → protocolo+SDK → binding). Ortogonal: os 3 tiers dela são todos consumer = system; ela responde como o parceiro fala, esta spec responde quem opera e quem consome.

10. Adicionar um eixo ou uma célula

Eixo novo exige RFC + passar em R1 (§3). Valor novo num eixo existente (ex.: operator = partner pra um revendedor que opera pelo cliente) exige RFC. Motivo novo em withheld (§5) exige edição desta spec. Nenhum dos três é inferível — todos são decisão registrada.

References