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.
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_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 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> → 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. *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 client3. Isolamento at storage layer
Postgres / kdb-next:
- *LS*(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 *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 / rate
limit 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 *xplicitlymarked*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 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
- *peed > Quality*(de stack-principles.kmd): adicionar tenant
desde o início custa um pouco de tempo. *esolution* a policy manda o trade
off — multitenant vence; speed concedida em outras arenas. - Nenhum direto entre policies. *eforç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 → *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. - ✗ Pre
policy: `~.claudestate/usage*.json` no filesystem do devLXC (sync via cron entre máquinas) — flat-file por máquina, não per
tenant. Será migrado pro kdbnext viaservices/ai/gateway(ver follow-ups emservices/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.