Multi-tenant by default
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.
Princípio mandatory: todo módulo da Koder Stack é multi-tenant unbounded desde o primeiro commit. Tenant ⊆ {
koder_user_id,workspace_id}. Single-tenant ou shared-state global é anti-pattern.
Por que
A Koder Stack alvo é N contas individuais + N workspaces (RFC-001 do kdb-next: 100M+ tenants target). Adicionar tenancy retroativamente é refactor doloroso:
- Schema migration de table sem
tenant_idpra comtenant_idexigebackfill de cada row, o que pode ser inviável em tabela ativa multi-million.
- Index composto
(tenant_id, …)precisa ser construído online; podesaturar I/O por horas.
- API endpoints sem auth-resolved tenant precisam ser breaking-changed.
- 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 fazer certo desde o começo: praticamente zero. Custo de retrofit: dias-a-semanas 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 parte 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> → countS3 / 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. Cliente 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 client3. Isolamento at storage layer
Postgres / kdb-next:
- RLS (Row-Level Security) habilitado em toda tabela com
tenant_id. PolicyUSING (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 com audit row em cada read fora-de-tenant (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 só dados do dono do auth.
- Sem auth → 401.
- Recurso de outro tenant → 404 (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, nãoΣ items. - Index
(koder_user_id, …)desde o início — não retroativo. - TTL / compaction / retention aplicáveis per-tenant.
- Cota / rate-limit per-tenant, 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 legitimamente 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_*- Nunca misturadas com per-tenant na mesma tabela
- RLS dessas tabelas:
USING (true)ou sem RLS, mas *xplicitlymarked*no schema comment
8. Storage tier choice (tech-agnostic)
Esta policy é agnó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 não muda.
9. Workspaces
workspace_id é nullable mas semanticamente carregado:
workspace_id IS NULL→ recurso pessoal dokoder_user_idworkspace_id IS NOT NULL→ recurso do workspace; users doworkspace 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
- Speed > Quality (de stack-principles.kmd): adicionar tenant
desde o início custa um pouco de tempo. Resolution: a policy manda o trade-off — multi-tenant vence; speed concedida em outras arenas.
- Nenhum direto entre policies. Reforça
hyperscale-first(eixodistinto: 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_idem tabela com PII → error (block release) - API sem auth → error
- Cache key sem prefixo → warning (advisory até audit hardener)
Migration path
Componentes pré-policy (existentes em 2026-05-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. - ✗ Pre-policy:
~/.claude/state/usage-*.jsonno filesystem do devLXC (sync via cron entre máquinas) — flat-file por máquina, não per-tenant. Será migrado pro kdb-next via
services/ai/gateway(ver follow-ups emservices/ai/gateway/backlog/).
Significance
Sem essa policy, IAs e engs novos podem escrever código "escalável" mas single-tenant — passa hyperscale-first audit, falha silenciosamente quando o primeiro segundo usuário entra. O custo de fazer errado só 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.