Multi-tenant by default

mandatory

Todo schema, API, storage e fluxo de dados na Koder Stack é desenhado desde o primeiro commit considerando N koder_user_id e N workspace_id independentes, com isolamento estrito entre tenants. Single-tenant ou shared-state global é anti-pattern. "Vamos abstrair depois" não é válido — adicionar tenant_id retroativo é refactor doloroso e error-prone.

Policy: Multi-tenant by default

Princípio *andatory* todo módulo da Koder Stack é multi-tenant unbounded desde o primeiro commit. Tenant ⊆ {koder_user_id, workspace_id}. Singletenant ou sharedstate global é anti-pattern.

Por que

A Koder Stack alvo é * contas individuais + N workspaces*(RFC-001 do kdb-next: 100M+ tenants target). Adicionar tenancy retroativamente é refactor doloroso:

  • Schema migration de table sem tenant_id pra com tenant_id exige

    backfill de cada row, o que pode ser inviável em tabela ativa multi-million.

  • Index composto (tenant_id, …) precisa ser construído online; pode

    saturar I/O por horas.

  • API endpoints sem authresolved tenant precisam ser breakingchanged.
  • Bugs de isolamento (cross-tenant leak) só aparecem em produção, com

    dado real de outros usuários — incidente de segurança garantido.

Custo de *azer certo desde o começo* praticamente zero. Custo de *etrofit* diasasemanas de trabalho + risco de leak.

Cláusulas-chave

1. Schema obrigatório

Toda tabela / coleção / KV namespace / partition que armazena dado de usuário tem koder_user_id (e opcionalmente workspace_id) como *arte da chave primária ou único índice de busca* Constraint NOT NULL é não-negociável em SQL; em KV / NoSQL, a key sempre tem prefixo de tenant.

-- ✓ ok
CREATE TABLE x (
  id BIGSERIAL,
  koder_user_id BIGINT NOT NULL,
  workspace_id BIGINT,           -- nullable: dado pessoal vs workspace
  payload JSONB,
  PRIMARY KEY (koder_user_id, id)
);

-- ✗ anti-pattern
CREATE TABLE x (
  id BIGSERIAL PRIMARY KEY,      -- sem tenant
  user_id BIGINT,                -- nullable, default 0 = bypass
  payload JSONB
);

KV / Redis:

✓ ok      tenant:rate_limit:<koder_user_id>:<window>  →  count
✗ anti    rate_limit:global:<window>                  →  count

S3 / object storage:

✓ ok      koder/<koder_user_id>/<workspace_id>/<resource_id>
✗ anti    koder/uploads/<resource_id>

2. Auth resolve tenant

Cada request resolve koder_user_id a partir do PAT, OAuth token ou session. *liente não passa tenant em header / query param.*Ignorar essa regra abre a porta pra IDOR (Insecure Direct Object Reference).

✓ ok      Authorization: Bearer <PAT>     → server resolves user
✗ anti    GET /api/x?user_id=42            → trust client
✗ anti    X-Tenant-ID: 42 header           → trust client

3. Isolamento at storage layer

Postgres / kdb-next:

  • *LS*(Row-Level Security) habilitado em toda tabela com

    tenant_id. Policy USING (koder_user_id = current_setting('koder.uid')::BIGINT).

  • Cada conexão chama SET LOCAL koder.uid = <auth-resolved-user>

    antes de queries.

KV / NoSQL: enforcement em camada de aplicação *om audit row*em cada read foradetenant (deve ser exceção excepcional, não rotina).

Read paths sem WHERE koder_user_id = $1 (ou equivalente key-prefix) = bug crítico → reverter.

4. API surface scoped

  • Endpoints retornam *ó*dados do dono do auth.
  • Sem auth → 401.
  • Recurso de outro tenant → *04 (não 403)* não vazar existência.
  • Compartilhar dados entre tenants exige decisão consciente

    (workspace, sharing primitives explícitos).

5. Hyperscale aplicado por-tenant

  • Cardinality bound = N tenants × M items, *ão*Σ items.
  • Index (koder_user_id, …) desde o início — não retroativo.
  • TTL / compaction / retention aplicáveis per-tenant.
  • Cota / ratelimit pertenant, não global.

6. Anti-padrões explícitos

Anti-pattern Correto
Variável global mutável compartilhada entre tenants map[koder_user_id]State ou storage scoped
Cache key sem prefixo de tenant cache:tenant:<uid>:<key>
Storage path hard-coded (/var/data/usage.json) /var/data/<tenant>/usage.json (ou DB-stored)
Sequência ID compartilhada (SERIAL global) Composite (tenant_id, item_seq) ou UUID
SELECT * FROM table sem WHERE de tenant RLS automático ou WHERE explícito
Cron global que toca todos tenants em loop sem checkpoint per-tenant Job pool com checkpoint per-tenant + retry
File on disk no LXC dev (~/.claude/state/usage.json) DB-stored, com tenant_id

7. Exception path: dados realmente cross-tenant

Dados que *egitimamente*não são per-tenant existem:

  • Catálogo público do Hub (hub.koder.dev/apps/<slug> — visível pra todos)
  • Specs e policies do monorepo (este arquivo, por exemplo)
  • Templates e documentação institucional

Estes ficam em tabelas distintas com naming explícito:

  • public_packages, system_settings, catalog_*
  • *unca*misturadas com per-tenant na mesma tabela
  • RLS dessas tabelas: USING (true) ou sem RLS, mas *xplicitly

    marked*no schema comment

8. Storage tier choice (tech-agnostic)

Esta policy é *gnóstica ao DB* As cláusulas valem em SQL, KV, document DB, vector DB, time-series. O que muda é como cada tier implementa isolation:

Tier Isolation mechanism
SQL (Postgres / kdb-next) RLS + SET LOCAL koder.uid
KV (Redis) Key prefix tenant:<uid>:…
Document (MongoDB-style) Filter {koder_user_id: …} em todo find
Vector Filter por metadata {tenant: <uid>} em ANN
Time-series (Timescale) Hypertable index (koder_user_id, recorded_at)
S3 / object Path prefix <uid>/<workspace>/… + IAM policy

A escolha de tier é guiada por padrão de acesso (ver stack-RFC-001-kdb-as-unified-data-plane.kmd); a obrigação de isolation *ão muda*

9. Workspaces

workspace_id é *ullable*mas semanticamente carregado:

  • workspace_id IS NULL → recurso pessoal do koder_user_id
  • workspace_id IS NOT NULL → recurso do workspace; users do

    workspace o veem; non-members → 404

Membership em workspace é resolvido pela tabela workspace_member (canonical do Koder ID) — qualquer query cross-workspace passa por ela.

Conflicts with

  • *peed > Quality*(de stack-principles.kmd): adicionar tenant

    desde o início custa um pouco de tempo. *esolution* a policy manda o tradeoff — multitenant vence; speed concedida em outras arenas.

  • Nenhum direto entre policies. *eforça*hyperscale-first (eixo

    distinto: scale ≠ isolation), security.kmd, code-first.kmd.

Audit

A audit script (futura) varre schema migrations + endpoint definitions + KV-key naming pra detectar violações. Severity:

  • Schema sem tenant_id em tabela com PII → *rror*(block release)
  • API sem auth → *rror*
  • Cache key sem prefixo → *arning*(advisory até audit hardener)

Migration path

Componentes prépolicy (existentes em 202605-09): NÃO precisam refactor imediato, mas:

  • Próximo refactor substancial deve aplicar o policy
  • Novos endpoints / tabelas em componentes existentes seguem a

    policy mesmo que o resto do componente esteja em débito

  • Lista de débitos vai pra meta/docs/stack/registries/multi-tenant-debt.md

    (a ser criado quando primeira violação for descoberta)

Examples

  • ✓ Compliant: products/dev/flow/engine/credentials (RFC-003) —

    Credential.OwnerID + ScopeType + ScopeID é tenancy explícita.

  • ✓ Compliant: services/foundation/id — tenant_id no storage proto.
  • ✗ Prepolicy: `~.claudestate/usage*.json` no filesystem do dev

    LXC (sync via cron entre máquinas) — flat-file por máquina, não pertenant. Será migrado pro kdbnext via services/ai/gateway (ver follow-ups em services/ai/gateway/backlog/).

Significance

Sem essa policy, IAs e engs novos podem escrever código "escalável" mas singletenant — passa hyperscalefirst audit, falha silenciosamente quando o primeiro segundo usuário entra. O custo de fazer errado *ó aparece em produção, com dados reais* e é incidente de segurança.

A policy é o equivalente do "always use prepared statements" pra SQL injection: todo mundo já sabe, mas precisa estar escrito formalmente pra ser enforced.

Source: ../home/koder/dev/koder/meta/docs/stack/policies/multi-tenant-by-default.kmd