Always-on

mandatory

Todo componente Koder é desenhado pra estar **sempre no ar e operante**, em todos os sentidos: usuários em qualquer versão (N, N-1, N-2 dentro da janela suportada) conseguem usar; rollout de nova versão atravessando milhares de servidores por dias não causa quebra em ponta alguma; servidor cai, rede oscila, dependência falha — produto degrada com graça, nunca trava nem perde dado. Maintenance window é anti-padrão. Flag day é anti-padrão. "Atualize todo mundo antes de mudar" é anti-padrão. A inviolabilidade da experiência atravessa **versão, topologia, conectividade e surface**.

Policy: always-on

*rincípio mandatory* a Koder Stack está *empre no ar e operante* Toda decisão de design considera, desde o primeiro commit, que diferentes versões do mesmo componente vão coexistir em produção, que rede e dependências falham, que clientes ficam offline, que rollouts levam dias, e que o usuário não pode perceber nada disso. Disponibilidade não é feature — é piso.

Por que

Alvo da Stack: *00M+ contas ativas, multiregião, multisurface* Nesse regime, três fatos são inescapáveis:

  1. *ollout não é instantâneo.*Atualizar uma frota de milhares de

    servidores + bilhões de clients (mobiledesktopTV/web) leva *ias ou semanas* Durante essa janela, N, N1, e às vezes N2 coexistem em produção. Nenhuma versão da matriz pode quebrar.

  2. *alhas são contínuas, não excepcionais.*Em qualquer minuto,

    algum DC tá com latência elevada, algum link cai, algum hardware morre, alguma dependência rate-limita. "Funciona quando tudo tá ok" é equivalente a "não funciona".

  3. * usuário não negocia.*O usuário com Koder Talk v3.2 no celular

    antigo e Koder Flow v4.1 no servidor self-hosted da empresa *ão tem que saber disso* Ele aperta o botão. Tem que funcionar.

Custo de *azer certo desde o começo* discipline + cobertura de testes. Custo de *etrofit* incidentes, suporte 24/7 caro, perda de confiança, churn. Esta policy existe pra que toda IA / engenheiro saiba que disponibilidade é loadbearing igual segurança ou multitenancy — não opcional, não "a gente vê depois".

Dimensões — visão geral

# Dimensão O que protege
§1 Coexistência de versões N / N1 / N2 Rollout multi-dia atravessa a frota sem quebrar nenhuma combinação
§2 Wireformat estável + forwardcompatibility Cliente novo lê payload velho; cliente velho lê payload novo
§3 Migrações de schema zero-downtime Schema evolui sem janela, sem flag day, sem coordenação global
§4 Sem janela de manutenção Deploy nunca derruba; restart nunca é "todo mundo ao mesmo tempo"
§5 Graceful degradation Dependência caída → feature desabilita com mensagem clara, resto opera
§6 Resumabilidade + tolerância offline Operação interrompida continua de onde parou; cliente offline guarda fila
§7 Multi-região / sem SPOF Queda de DC degrada latência, não derruba o produto
§8 Cross-surface compatibility Mobile v5 conversa com desktop v3; handshake negocia capacidade

Cada dimensão tem cláusulas normativas (**, templates de teste (** e gate de release.


§1. Coexistência de versões N, N1, N2

R1.1 — Janela suportada

Todo componente declara explicitamente sua *anela de versões coexistentes*em koder.toml:

[compat]
window_minor_versions = 2     # N, N-1, N-2 devem interoperar
window_major_versions = 1     # major bump é breaking — separado
window_duration_days  = 180   # tempo mínimo de suporte a N-1 em produção

Default Stack-wide: * minor + 1 major + 180 dias* Componentes podem TIGHTEN (mais janela), nunca LOOSEN.

R1.2 — Capability handshake

Toda conexão cliente↔servidor inicia com *andshake de capability*

  • Cliente envia: {client_version, supported_features: [...]}
  • Servidor responde: {server_version, accepted_features: [...]}
  • Cliente opera no *ntersection*das features aceitas

Anti-padrão: cliente assume features pelo número de versão do servidor. Versão é informativa; *apability é normativo*

R1.3 — Nenhum endpoint exige "estamos todos na mesma versão"

Endpoints, RPCs, protocolos, eventos: todos têm que tolerar contraparte em qualquer versão dentro da janela. *unca*

  • "404 because client is too old" → degrade, não falhe
  • "500 because field is missing" → trate como ausente, não como erro
  • "Force upgrade" pop-up bloqueante → só permitido após R1.1 expirar

R1.4 — Deprecation com prazo, não com flip

Remover featurecampoendpoint segue 3 fases:

  1. *nnouce*(release N): marca deprecated, log warning server-side,

    doc no CHANGELOG, ticket de migração consumidores

  2. *uiet*(release N+1, ≥ 90 dias): feature ainda funciona, sem

    warning ruidoso; consumidores conhecidos já migraram

  3. *emove*(release N+2, ≥ 180 dias após N): feature sai

Pular fase = violação. Bug fix de segurança que requer remoção imediata vai pro exception path (§ Exceptions).


§2. Wireformat estável + forwardcompat

R2.1 — Unknown-field tolerance é mandatório

Toda biblioteca de serialização usada por componente Koder DEVE preservar campos desconhecidos em round-trip:

  • Protobuf: unknown_fields preservado (default proto3 ≥ 3.5)
  • JSON: parser preserva extra keys e re-serializa; nunca strict: true

    em path crítico de wire

  • KMD / KVG / KPKG: parser TEM que skipar diretivas desconhecidas (já

    é spec — ver specs/document-format.kmd)

  • gRPC / RPC custom: idem proto

Anti-padrão: json.Unmarshal em struct sem json:",omitempty" + re-serialização dropando campos do payload original. Cliente N+1 manda campo novo, gateway N strips, servidor N+1 não recebe → bug.

R2.2 — Versão de schema é first-class

Todo payload de wire (RPC, evento, file format) carrega *chema version tag*no envelope:

{ "v": 3, "kind": "MessageDelivered", "payload": { ... } }

Schema bump é semver:

  • *atch*(3.0 → 3.1): só adiciona campos opcionais
  • *inor*(3.x → 4.0): adiciona campos required dentro da janela

    (com default sane no parser legado)

  • *ajor*(3.x → 4.0 breaking): rara; passa por R1.4 deprecation

R2.3 — Enum é extensível

Enums em wire format SEMPRE têm valor unknown = 0 (ou equivalente) e parser trata unknown como graceful degradation:

enum MessageKind {
  MESSAGE_KIND_UNSPECIFIED = 0;   // forward-compat reserved
  MESSAGE_KIND_TEXT = 1;
  MESSAGE_KIND_IMAGE = 2;
  // future: MESSAGE_KIND_VIDEO = 3 (cliente N entende como UNSPECIFIED)
}

Anti-padrão: enum fechado sem fallback → cliente velho recebe valor novo, crash em switch exhaustive.

R2.4 — Forbidden breaking wire changes (sem deprecation)

Mudança Status
Adicionar campo opcional ✓ OK qualquer momento
Adicionar enum value ✓ OK qualquer momento
Adicionar endpoint novo ✓ OK qualquer momento
Mudar tipo de campo existente ✗ Major + deprecation
Renomear campo ✗ Adicionar novo + deprecate antigo (R1.4)
Remover campo ✗ Major + deprecation
Mudar semântica sem mudar tipo ✗ Pior caso possível — proibido

§3. Migrações de schema zero-downtime

R3.1 — Padrão expand → migrate → contract

Toda migração de schema (SQL, kdb, KV namespace, file layout) que muda forma de dado segue * releases*mínimo:

  1. *xpand*(release N): adiciona nova forma; código N escreve em

    *mbas*as formas; lê preferencialmente da nova, fallback pra antiga

  2. *igrate*(entre N e N+1): backfill assíncrono converte rows

    antigas pra forma nova (job em background, idempotente, resumível, per-tenant)

  3. *ontract*(release N+1, depois de migrate confirmado): código

    pára de escrever na forma antiga; opcionalmente dropa coluna/tabela

Validar que *+1 e N coexistem*durante o intervalo de rollout.

R3.2 — NOT NULL só depois de backfill

Adicionar NOT NULL a coluna existente é proibido sem 3 passos:

  1. ADD COLUMN (nullable, com default sane se aplicável)
  2. Código novo preenche em todo write; backfill async preenche rows

    velhos

  3. *epois de confirmar 100% preenchido*→ ALTER TO NOT NULL

Postgres / kdb: ALTER NOT NULL direto em tabela grande TRAVA escritas — é um maintenance window disfarçado. Proibido.

R3.3 — DROP é a coisa mais cara

Dropar coluna, tabela, índice, KV namespace, file blob:

  1. Stop reading (release N): código não lê mais; ainda escreve
  2. Stop writing (release N+1): código não toca mais
  3. *ait ≥ janela R1.1*depois do release N+1 entrar em 100% da

    frota

  4. Drop fisicamente (release N+2 ou migration manual)

Se a janela R1.1 é 180 dias, dropar coluna leva ~6 meses calendartime. Esse é o preço de alwayson.

R3.4 — Online DDL obrigatório

Toda DDL em tabela ativa usa modo online do DB:

  • Postgres: CREATE INDEX CONCURRENTLY, ALTER TABLE ... ADD COLUMN ... DEFAULT ... sem rewrite (Postgres ≥ 11), pg_repack pra reorder
  • kdb: schema change via versioned descriptor; nunca lock global
  • KV: novo namespace + dual-write + cutover

DDL bloqueante em tabela > 10k rows em produção = release block.


§4. Sem janela de manutenção

R4.1 — Deploy é rolling, sempre

Nenhum componente Koder tem caminho legítimo de "stop everything, deploy, start". Toda release suporta:

  • *olling restart* instâncias atualizam uma de cada vez, traffic

    drena, healthcheck confirma antes de seguir

  • *oexistência durante rollout* instâncias N e N-1 servindo tráfego

    ao mesmo tempo, possivelmente por horas

R4.2 — Healthcheck reflete prontidão real

/healthz (ou equivalente) só responde 200 quando:

  • Dependências essenciais respondem
  • DB pool aquecido
  • Cache pré-populado (se aplicável)
  • Capability handshake operacional

Anti-padrão: /healthz retorna 200 desde o main() — load balancer manda tráfego pra instância que ainda não conecta no DB → 500 pro usuário.

R4.3 — Graceful shutdown

SIGTERM → drain phase (≥ 30s default, configurable):

  1. Pára de aceitar conexões novas
  2. Termina conexões em-flight (com timeout sane)
  3. Flush de buffers / cache write-back / commit de transações
  4. Exit clean

Conexão long-lived (WebSocket, gRPC stream, SSE): server manda *econnect hint*ao client antes de derrubar; client reconecta em instância nova.

R4.4 — "Maintenance mode" é proibido como default

Página/banner "estamos em manutenção" só é aceitável em *ncidente declarado*(perda de DC, exploit ativo). Nunca como ferramenta de rollout. Toda PR que introduz maintenance toggle em path normal = block.


§5. Graceful degradation

R5.1 — Falha de dependência opcional ≠ falha do produto

Dependência indisponível → feature dependente desabilita com mensagem clara; o resto do produto continua funcional.

Exemplos concretos:

Dependência cai Comportamento correto
AI gateway lento Botão "Gerar com IA" mostra spinner + fallback "tente mais tarde"; resto do editor funciona
Search index morto Busca avisa "indisponível"; navegação manual funciona
Push notification rejeita Mensagem chega quando user abre o app (pull funciona)
Telemetry endpoint 503 Buffer local + retry; *unca*bloqueia ação do user
Auto-update server timeout App roda na versão atual; tenta de novo depois

Anti-padrão: erro 500 do gateway de notificação → app trava tela do user.

R5.2 — Dependência essencial: fail-fast + circuit breaker

Dependência sem a qual a feature não pode operar:

  1. Circuit breaker (típico: 5 falhas em 30s → abre por 60s)
  2. Erro userfacing claro (`specserrorsuserfacing-messages.kmd`)
  3. Telemetria de degradação para oncall
  4. Re-tenta no fundo; recupera sozinho quando dep volta

R5.3 — Timeout em toda I/O network

context.WithTimeout (Go) / equivalente em qualquer linguagem: *enhuma*chamada de rede sem timeout. Default sane (5s pra intraDC, 30s pra widearea). Timeout estourar é graceful degradation, não panic.

R5.4 — Retry com backoff exponencial + jitter

Retry sem jitter cria *hundering herd*depois de incidente. Mandatório:

  • Backoff exponencial (base 100ms, factor 2, max 30s)
  • Jitter ≥ 25% (decorrelação)
  • Cap de retries (5 default)
  • Idempotency-key em retry de mutation

§6. Resumabilidade + tolerância offline

R6.1 — Operações longas são resumíveis

Upload, download, sync, import, export, batch processing: cada um tem checkpoint. Conexão cai / processo morre / power cut → operação retoma de onde parou na próxima tentativa.

Protocolo:

  • Upload: chunked (chunks ≤ 8 MiB), server confirma cada chunk,

    cliente persiste cursor local; resume = "continue do cursor"

  • Download: HTTP Range request (Range: bytes=N-)
  • Sync: vector clock / lamport timestamp / opaque cursor + tombstones
  • Batch: peritem commit + pertenant checkpoint

R6.2 — Cliente offline é estado normal

Mobile / desktop apps assumem que *onectividade é intermitente*

  • Toda ação grava local primeiro (SQLite, kdb embarcado, file)
  • Fila de outbound operations persistente
  • Sync ao reconectar; conflict resolution explícita (lastwriterwins é raramente correto)
  • UI mostra estado: synced / pending / failed
  • *ead*continua funcionando 100% offline em features locais

Anti-padrão: app sem conexão mostra tela branca / erro genérico / desabilita botão sem explicação.

R6.3 — Idempotência de mutations

Toda operação que altera estado server-side aceita *dempotency key*(cliente gera UUID v4 / ULID, repete em retry):

POST /messages
Idempotency-Key: 01HX...
{ "thread_id": ..., "text": "..." }

Servidor garante: 2 requests com mesma key → mesmo efeito + mesma resposta. Sem idempotency: retry de cliente offline duplica mensagem quando volta online.

R6.4 — Sessão sobrevive a reconnect

WebSocket / SSE / gRPC stream: cliente reconecta dentro de janela (≥ 5 min default) e recupera state sem re-autenticar.


§7. Multi-região / sem SPOF

R7.1 — Nada que toca usuário fica single-region

Storage de dado de usuário, identity service, gateway de tráfego, DNS: tudo replicado entre ≥ 2 regiões geograficamente distintas.

Single-region OK só pra:

  • Componente interno (build farm, CI)
  • Sandbox / dev / staging
  • Catálogo público read-only (com CDN cache global)

R7.2 — Failover é automático

DC down → traffic re-routa em ≤ 60s sem ação humana:

  • DNS com health-check (≤ 30s TTL pra A/AAAA críticos)
  • Anycast onde aplicável
  • Replicação de DB async cross-DC; failover promove read replica
  • Healthcheck em cada região; orchestrator (ou Jet) tira região doente

R7.3 — Read replicas próximas do usuário

Latência percebida é orçamento finito. Reads críticos (homepage, feed, search) servem da região mais próxima do user (geo-DNS ou anycast).

R7.4 — Backups são uma forma de "always-on"

Perda de dado é o oposto extremo de always-on. Backup:

  • Snapshot incremental ≥ 1×/hora pra dado mutable
  • Offsite (crossregion + cold storage) ≥ 1×/dia
  • Restore testado mensalmente em ambiente isolado
  • Ver policies/checkpoint-retention.kmd pra retenção

§8. Cross-surface compatibility

R8.1 — Surfaces conversam por contrato, não por versão

Mobile / desktop / TV / web / CLI do mesmo produto Koder podem estar em versões diferentes do SDK (koder_kit, koder_web_kit, etc.) e do produto. Quando trocam state (sync, IPC, shared session, KMD doc compartilhado), trocam por *ontrato versionado*(R2.2 + R1.2).

R8.2 — Format de documento é forward-compat por construção

KMD, KVG, KPKG, qualquer file format Koder: parser ignora diretivas desconhecidas e preserva no save round-trip. Doc criado em desktop v3 abre em mobile v2 (sem features novas, sem perder bytes); abre em desktop v4 (com features novas) sem upgrade ritual.

R8.3 — IPC entre apps Koder (ver specs/ipc/protocol.kmd)

Protocolo IPC negocia versão no handshake. App receiver descobre que sender tá em versão antiga → entrega só features comuns. Nunca crasha por payload "muito novo" ou "muito velho".

R8.4 — Coverage matrix

Cada produto Koder publica em registries/variant-compat-matrix.md qual combinação de surfaces × versões foi testada. Combinações não testadas dentro da janela R1.1 = bug latente; CI bloqueia release sem coverage mínimo (T8 abaixo).


Templates de teste mandatórios

T1 — Matriz de versões N × N-1

Para cada componente client+server: rodar suite em *odas*as combinações dentro da janela R1.1.

client v3.2 ↔ server v3.2  ✓
client v3.2 ↔ server v3.1  ✓
client v3.1 ↔ server v3.2  ✓
client v3.0 ↔ server v3.2  ✓ (N-2)
client v3.2 ↔ server v3.0  ✓

Implementação: tests/compat/ com dockercompose multiversion ou matrix CI.

T2 — Unknownfield roundtrip

Parser recebe payload com campo novo desconhecido → preserva no reemit. Asserção byteequal (depois de normalização) em todas as libs de serialização que o componente usa.

T3 — Rolling upgrade simulado

Em ambiente de teste: 3 réplicas, fazer rolling restart com tráfego ativo (load generator), assertir 0 erros 5xx percebidos pelo cliente durante a janela.

T4 — Schema migration em produção-likeness

Migration aplicada em snapshot de prod-like (millions of rows), medir locks, downtime, falha de queries concorrentes. Reject se qualquer query bloqueia > 100ms.

T5 — Chaos test de dependência opcional

Mata dependência opcional (AI gateway, search, notification): produto continua respondendo 200 nas features que não dependem dela; feature dependente degrada conforme R5.1.

T6 — Resumability de upload

Upload de 100 MiB. A 50%: kill -9 cliente. Reabrir cliente, retomar upload, assertir bytes finais iguais ao original (bytebybyte).

T7 — Offline tolerance de mobile/desktop

Disable network → execute fluxo principal → re-enable network → assert sync completa sem perda nem duplicação.

T8 — Cross-surface compat coverage

Matrix de surfaces (mobile × desktop × web × TV × CLI) × pares de versões. Mínimo: surface mais recente × surface mais antiga da janela.

T9 — Failover regional

Simula queda da region primária; mede tempo até traffic re-roteado; assertir < 60s e zero data loss.


Gate de release

Componente NÃO entra em release pipeline se:

  • Janela R1.1 não declarada em koder.toml
  • T1 falha em qualquer combinação na janela
  • Schema migration sem plano expandmigratecontract documentado
  • /healthz retorna 200 antes de dep essencial estar pronta (T3 detecta)
  • Operação longa sem checkpoint (T6 detecta no path crítico)
  • I/O network sem timeout (audit estático)
  • Cliente sem retry com jitter (audit estático)
  • Coverage matrix T8 < 80% da janela
  • Sem failover testado (T9) pra componente single-region servindo user data

Severity:

  • *rror*(block release): R1.1, R2.1, R2.3, R2.4, R3.1, R3.2, R4.1, R5.3, R6.3
  • *arning*(advisory): R5.4, R6.4, R7.3, T8 < 100%

Anti-padrões explícitos

Anti-pattern Por que dói Correto
"Atualizem todos os clientes antes do dia X" Flag day — impossível em escala R1.1 + R1.2
if version != current { return 426 Upgrade Required } Quebra metade da frota durante rollout Capability handshake R1.2
json.Unmarshal strict mode em wire path Drops campos novos R2.1
ALTER TABLE ADD COLUMN NOT NULL DEFAULT now() em tabela ativa Lock até backfill, downtime R3.2 expandmigratecontract
Maintenance page "voltamos em 2h" pra rollout Default = não disponível R4.4
panic em código de produção Mata processo, mata user Recovery + erro user-facing
Upload de 1 GB sem chunk Falha 80% das vezes em conexão móvel R6.1 chunked + resumable
Cron global sem checkpoint per-tenant Reprocessa tudo após crash R6.1 + multi-tenant
Single-region DB pra dado de user Queda do DC = perda do usuário R7.1
App offline mostra tela branca Sem comunicação = sem confiança R6.2
Retry sem jitter Thundering herd R5.4
Enum exhaustive switch sem default Crash em valor novo R2.3

Exceptions

Caminho legítimo pra burlar always-on:

  1. *ecurity fix com remoção imediata necessária*— CVE crítico

    exige drop de feature antes da janela expirar. Aprovado casoacaso pelo owner; documentado em meta/context/incidents/.

  2. *ev / staging environments*— testes destrutivos OK em ambientes

    internos.

  3. *omponentes prépolicy (≤ 202605-24)*— não exige refactor

    imediato; próximo refactor substancial aplica. Lista em meta/docs/stack/registries/always-on-debt.md (a ser criado quando primeira violação for descoberta).

Exception NÃO é:

  • "Tô com pressa pra essa release" → no.
  • "Esse componente é só interno" → ainda assim, dev experience é always-on.
  • "Mas é só um experimento" → experimentos vivem em Koder/sandbox,

    fora do gate de release; produção tem o gate.


Conflicts with / composes with

  • *policies/hyperscalefirst.kmd`*(escala): alwayson é o eixo

    de disponibilidade; hyperscale é o eixo de throughput. Os dois são ortogonais e ambos mandatórios.

  • *policies/multitenantbydefault.kmd`* migrações zerodowntime

    da §3 aplicam *er-tenant* nunca derrubam ou trancam tenants alheios à mudança.

  • *policies/headlessfirst.kmd`* testes T1T9 acima são headless

    por construção; usa o mesmo SDK de teste canônico.

  • *policies/releases.kmd`* rolling deploy + janela R1.1 entram

    no pipeline.

  • *policies/stackprinciples.kmd`*(Speed > Quality): alwayson

    vence; "ship rápido" não justifica flag day.

  • *specsvariantstaxonomy.kmd`* matriz de variantes × versões

    é o que T8 cobre.

Migration path

Componentes prépolicy (existentes em 202605-24): não exige refactor imediato.

  • Próxima release MAJOR de cada componente aplica esta policy
  • Novos endpoints / wire formats / schemas em qualquer componente

    seguem a policy desde o primeiro commit (mesmo que o resto do componente esteja em débito)

  • Débitos catalogados em meta/docs/stack/registries/always-on-debt.md
  • Auditoria automática (futura): koder-spec-audit always-on --scope <component>

Significance

Sem essa policy, o Stack pode passar todos os outros gates (hyperscale, multi-tenant, security, headless) e ainda assim viver em incidentemododefault: cada release é uma manhã estressante, cada servidor antigo é uma bomba-relógio, cada cliente desatualizado é um ticket de suporte, cada DC com problema é um post-mortem. * diferença entre "produto Koder" e "produto Koder operável em escala planetária" mora aqui.*

A policy é o equivalente de "always use TLS" pra cleartext network: todo mundo sabe; precisa estar escrito formalmente pra ser enforced.

Source: ../home/koder/dev/koder/meta/docs/stack/policies/always-on.kmd