Pular para o conteúdo
Nelcota
EN : Read in English

Registro de decisões

43 min de leitura Editar no GitHub

Decisões históricas durante a construção do Nelcota, com seus motivos. As mais recentes ficam no fim. Marcos anteriores são snapshots: a fonte atual do servidor exige Rust 1.99, o SDK Rust suporta 1.88, e um CLI sem configuração de servidor mostra ajuda. Prefira guias atuais e notas de versão a escolhas substituídas. Números das decisões e justificativas históricas permanecem intactos.

Marco 1

D1. Licença Apache-2.0. Escolhida pelo mantenedor (2026-10-06).

D2. Nome e binário. O projeto se chama Nelcota; o binário único é nelcota (crate nelcota-server). No Marco 4, recebe subcomandos (nelcota init, nelcota up...); sem argumentos, inicia o servidor.

D3. Os crates admin e cli só chegam nos Marcos 4/5. Evita crates vazios.

D4. Testes de integração em crates/server/tests/. O workspace é virtual (não há pacote na raiz), e testes de integração do Cargo devem pertencer a um pacote. server expõe o Router em uma lib para que os testes usem o mesmo código da produção.

D5. Uma única URL. NELCOTA_DATABASE_URL aponta para um papel proprietário do esquema (o superusuário postgres no Docker) e é usada apenas para migrações e para definir a senha de authenticator. A API usa o mesmo host/banco, trocando usuário/senha por authenticator / NELCOTA_AUTHENTICATOR_PASSWORD.

D6. A senha de authenticator fica fora das migrações. Migrações não contêm segredos. Na inicialização, o servidor calcula o verificador SCRAM-SHA-256 no lado do cliente e executa ALTER ROLE ... PASSWORD 'SCRAM-SHA-256$...': a senha em texto puro nunca trafega na rede nem aparece no log de instruções do Postgres.

D7. set_config('role', $1, true) em vez de SET LOCAL ROLE x. Ambos são equivalentes (SET LOCAL ROLE é açúcar sintático para set_config('role', ..., true)), mas set_config recebe um parâmetro, define papel e claims em uma única ida e volta e evita SQL construído como string. O valor do papel vem de um enum fechado.

D8. Pool com RecyclingMethod::Fast. Papel e claims têm escopo de transação e terminam no COMMIT/ROLLBACK (incluindo descarte sem commit, que envia ROLLBACK). Não é necessário DISCARD ALL a cada retirada. Coberto pelos testes de vazamento do pool e rollback em crates/server/tests/rls.rs.

D9. Migrações internas em nelcota.schema_migrations. Um esquema próprio, sem GRANTs para os papéis da API, para não aparecer em public (exposto pela API automática). Um bloqueio consultivo serializa a inicialização entre instâncias.

D10. A tabela de exemplo fica fora das migrações. examples/todos.sql é aplicado pelos testes e opcionalmente em desenvolvimento. As migrações do Nelcota não criam tabelas de negócio no banco de ninguém.

D11. Extensões no esquema extensions. pgcrypto e pg_stat_statements ficam fora de public. pg_stat_statements exige shared_preload_libraries=pg_stat_statements (já presente no compose de desenvolvimento e nos testes).

D12. JWT HS256 provisório (substituído por D20). O Marco 1 usa HS256 com segredo compartilhado (≥ 32 caracteres) por trás do trait JwtVerifier. O Marco 2 adiciona EdDSA + JWKS como padrão, e HS256 permanece como modo simples.

D13. Regras das claims. role é obrigatória e deve ser anon, authenticated ou service_role. authenticated exige um sub UUID (para que auth.uid() nunca falhe na conversão). aud ainda não é verificada (entra no contrato no Marco 2). Tolerância de 30 s em exp.

D14. Sem token = anon; token inválido = 401. Um cabeçalho Authorization presente, mas inválido (expirado, assinatura incorreta, esquema diferente de Bearer, papel desconhecido) nunca se torna anon silenciosamente.

D15. 401 versus 403 em permissão ausente. insufficient_privilege (42501) se torna 401 para anon (precisa se autenticar) e 403 para os demais papéis, como no PostgREST.

D16. rsa ignorado em cargo audit/cargo deny (RUSTSEC-2023-0071). Vem pelo backend rust_crypto de jsonwebtoken. O Marvin Attack afeta operações com a chave RSA privada; Nelcota nunca assina com RSA. O backend aws_lc_rs evitaria a dependência, mas exige ferramentas C/CMake na compilação, o que prejudica o "binário estático simples". Reavaliar se RS256 for usado para assinatura.

D17. Bibliotecas fora da tabela do prompt.

  • uuid: valida a claim sub e é o tipo nativo do Postgres para IDs.
  • postgres-protocol: já é dependência transitiva de tokio-postgres; usado para calcular o verificador SCRAM (D6).
  • http-body-util (apenas desenvolvimento): lê corpos de resposta nos testes.

D18. Postgres sem TLS na rede interna. O banco só é acessível pela rede interna do Docker (ou localhost em desenvolvimento). TLS para o banco aguarda um cenário de banco remoto.

D19. Edition 2024, MSRV 1.88. O testcontainers-modules atual já exige 1.88.

Marco 2

D20. EdDSA (Ed25519) como padrão; HS256 como legado. A chave privada vem de NELCOTA_JWT_PRIVATE_KEY (PKCS#8 PEM ou base64 em uma linha, que cabe em um .env). Com chave EdDSA e segredo HS256 configurados, o servidor assina com EdDSA e aceita ambos na verificação (migração sem indisponibilidade). O kid é a impressão digital RFC 7638.

D21. Papel interno nelcota_auth para o esquema auth. Handlers de autenticação executam com SET LOCAL ROLE nelcota_auth (string fixa). Nenhum JWT pode assumi-lo (o enum Role não o contém), e os papéis da API não têm GRANT em auth.*. Alternativa descartada: funções SECURITY DEFINER, que espalhariam lógica em PL/pgSQL sem ganho real.

D22. Token de renovação opaco de 32 bytes, armazenado apenas como SHA-256. SHA-256 (sem argon2) porque o token já tem 256 bits de entropia; a busca ocorre por índice único, sem comparação no aplicativo.

D23. A detecção de reutilização revoga a sessão (família), não o usuário. Outras sessões permanecem válidas. Sem janela de tolerância por enquanto (documentado).

D24. Logout revoga a sessão; o JWT de acesso é válido até exp. Tokens de acesso não mantêm estado e são curtos (15 min por padrão). Verificar revogação em cada requisição custaria uma ida e volta extra ao banco.

D25. Argon2id com os parâmetros padrão do RustCrypto (m=19 MiB, t=2, p=1). Executa em spawn_blocking, com no máximo min(cores, 4) hashes concorrentes (pico < 80 MiB). Login com e-mail desconhecido verifica um hash fictício para que o tempo de resposta não revele se a conta existe.

D26. Limite de requisições em memória, janela fixa de 1 minuto (a janela se tornou GCRA em D69), por IP (cadastro/token) e por e-mail (login). Um binário por instalação dispensa Redis. O IP vem de X-Forwarded-For (entrada mais à direita) apenas com NELCOTA_TRUST_PROXY=true.

D27. Cadastro com e-mail duplicado responde 409. Sem confirmação de e-mail no MVP, ocultar a existência da conta no cadastro não traria proteção real (o login continua sem revelá-la).

D28. Novas bibliotecas. argon2 (estava na tabela); sha2, base64 e getrandom já eram dependências transitivas (jsonwebtoken/RustCrypto) e são usadas diretamente nas mesmas versões, sem aumentar a árvore. A licença ISC é permitida em cargo deny (vem de simple_asn1, usado para ler PEM).

Marco 3

D29. SQL dinâmico com construtor próprio, sem sea-query. Todo valor da URL se torna um parâmetro text, e o Postgres o converte para o tipo da coluna ($1::text::<catalog type>); inserções e atualizações usam json_populate_recordset/json_populate_record, e RPC usa json_to_record. Assim o Postgres valida tipos como faria com um literal, e o aplicativo não precisa mapear tipos. sea-query vincula cada valor a um tipo Rust (e sea-query-postgres exigiria esse mapeamento), além de não expressar bem esses padrões. O construtor tem ~400 linhas com três regras auditáveis: identificadores apenas do catálogo e sempre entre aspas; tipos de conversão apenas do catálogo; valores sempre parametrizados. Testes unitários e de integração cobrem injeção nos dois lados.

D30. OpenAPI construído com serde_json, sem utoipa. utoipa gera a especificação em tempo de compilação a partir de tipos Rust; a nossa vem do catálogo em tempo de execução. O documento é filtrado pelos privilégios do papel da requisição (como o modo follow-privileges do PostgREST).

D31. Recarga do catálogo por event trigger + LISTEN nelcota. O trigger executa em ddl_command_end e sql_drop (incluindo GRANT/REVOKE). O listener agrupa rajadas (100 ms), reconecta com espera progressiva e recarrega a cada reconexão. Sem superusuário, a migração prossegue sem o trigger e a recarga é manual (NOTIFY).

D32. PATCH/DELETE exigem filtro. Diferentemente do PostgREST (que permite e deixa a RLS limitar), recusamos com 400. O custo é um filtro explícito quando a intenção realmente é atingir tudo.

D33. Lotes com chaves diferentes respeitam DEFAULT. Linhas são agrupadas por conjunto de colunas e cada grupo se torna um INSERT (CTEs em uma instrução). No PostgREST, sem Prefer: missing=default, colunas ausentes se tornam NULL; consideramos DEFAULT o comportamento menos surpreendente.

D34. statement_timeout no papel authenticator. ALTER ROLE anon SET ... não tem efeito com SET ROLE (o Postgres só aplica as configurações do papel no login). A inicialização aplica NELCOTA_STATEMENT_TIMEOUT_SECS (padrão 10 s) a authenticator, e o erro se torna 504.

D35. Mantido o padrão do Postgres para EXECUTE em funções (PUBLIC). Alterar os privilégios padrão ocultaria o comportamento normal do Postgres; a documentação e o painel avisam sobre isso.

D36. Novas bibliotecas. form_urlencoded, futures-util e bytes já eram dependências transitivas (axum/tokio-postgres); compression-gzip do tower-http adiciona flate2.

Marco 4

D37. Um binário, duas funções. nelcota sem argumentos (ou serve) inicia o servidor; os subcomandos operam a instalação. Em um host, migrate/types/token são encaminhados ao contêiner app (docker compose exec), que consegue alcançar o Postgres em uma rede internal: true; com NELCOTA_DATABASE_URL no ambiente, executam diretamente.

D38. Perfis do Postgres se tornam -c key=value no compose. Os arquivos deploy/postgres/profiles/*.conf são a origem; init os transforma em argumentos do comando postgres. Usar config_file substituiria todo o postgresql.conf da imagem (e mudaria o diretório padrão de pg_hba.conf).

D39. Backups com pg_dump -Fc; uploads S3 com a imagem amazon/aws-cli. Nada para instalar no host e nenhuma assinatura S3 própria (SigV4). Restauração em uma única transação (--single-transaction --exit-on-error). PITR com WAL-G está pendente (dumps diários: RPO de até 24 h).

D40. Atualização com reversão = imagem anterior + restauração do backup anterior à atualização. A nova versão pode ter aplicado migrações internas desconhecidas pela anterior (refinery recusa iniciar com uma migração "desconhecida"). Por isso, a reversão também restaura o banco e o snapshot correspondente dos arquivos em disco. O proxy permanece em manutenção desde antes de parar o aplicativo antigo até que o aplicativo atualizado ou restaurado esteja saudável, para que escritas públicas bem-sucedidas não se percam entre captura e reversão. Testes de falhas cobrem essa sequência.

D41. Imagem scratch com binário musl; verificação de saúde embutida. Sem shell ou curl na imagem: nelcota healthcheck executa GET /health por TCP simples.

D42. Migrações de usuário no formato do refinery (V<n>__<name>.sql), em nelcota.user_migrations, separadas das internas (nelcota.schema_migrations). CRLF é normalizado antes do checksum (um checkout Windows não diverge).

D43. install.sh instala Docker com o script oficial se estiver ausente (como root e sem NELCOTA_SKIP_DOCKER=1): isso torna os "3 comandos" possíveis em uma VPS nova. O checksum SHA-256 do binário é sempre verificado.

D44. init não ativa o firewall sem --firewall. Ativar ufw remotamente com a porta SSH errada bloqueia o acesso do proprietário à máquina. Por padrão, apenas recomenda a ativação.

Marco 5

D45. Painel com HTML no servidor + ~2 KB de JS próprio, sem HTMX (substituído por D50). Páginas são formulários comuns; a única parte interativa (o editor SQL) cabe em poucas linhas de fetch. CSP script-src 'self' sem unsafe-inline. Assets embutidos com rust-embed.

D46. Login do painel separado dos usuários finais. E-mail + hash argon2id no .env (gerados por init; senha exibida uma vez). Sessões em memória (reiniciar o servidor desconecta o administrador), cookie HttpOnly; SameSite=Strict (Secure atrás do proxy), limite de login e verificação de Origin/Sec-Fetch-Site em todo POST (CSRF).

D47. O painel usa a conexão administrativa. O administrador é dono do banco, como em psql. Escritas em tabelas reutilizam o construtor SQL da API (identificadores do catálogo, valores parametrizados). O editor SQL abre uma nova conexão por execução: BEGIN sem COMMIT ou SET ROLE não contaminam o pool. O texto SQL não vai para o log.

D48. Valores do painel mantêm o texto do Postgres (RawValue), sem passar por f64: numeric(30,10) é mostrado e editado com todos os dígitos.

Design do painel

D49 (substituído por D50). Painel com aparência de console de banco (escuro por padrão, destaque esmeralda), inspirado no Supabase, mas com nome, marca e ícones próprios. Tipografia Inter + JetBrains Mono, embutida no binário (~88 KB, OFL, veja crates/admin/assets/FONTS.md): sem Google Fonts, para manter a CSP em 'self' e funcionar offline. Modo claro via prefers-color-scheme. Ícones SVG desenhados à mão (sem pacote de ícones). Na visão geral, a contagem de linhas é exata para tabelas com menos de 10 mil linhas estimadas e usa a estimativa do planejador (≈) para as grandes.

Painel em Svelte

D50. Painel em Svelte 5 + shadcn-svelte + Tailwind v4 + CodeMirror 6, compilado com Vite (escolha do mantenedor, 2026-10-06). O objetivo é a interatividade que o HTML no servidor não entregava bem: edição inline na grade, painéis laterais, seleção múltipla, ordenação de colunas, editor SQL com destaque de sintaxe e preenchimento automático das tabelas e colunas do banco. Uma SPA com Vite, sem SvelteKit: ele injeta script inline na página, o que quebraria a CSP script-src 'self'. Roteamento próprio (~60 linhas, history API).

D51. A compilação (crates/admin/ui/dist) é versionada no git e embutida com rust-embed. Assim, cargo build, cargo install e o Dockerfile continuam sem precisar de Node: apenas quem trabalha no painel precisa dele. O job admin-ui da CI executa npm ci, npm audit, svelte-check, vite build e falha se o dist versionado estiver desatualizado.

D52. CSP: style-src agora aceita 'unsafe-inline'; script-src continua apenas com 'self'. Transições Svelte, CodeMirror e posicionamento de menus (floating-ui) injetam estilos em tempo de execução. Estilo inline não executa código; scripts inline continuam proibidos (há teste).

D53. O painel se torna uma API JSON em /admin/api/*. Mesma sessão, mesma verificação de origem (CSRF) e mesmo construtor SQL da API REST. Valores trafegam como texto exato do Postgres (RawValue). Exclusões em lote em uma transação. A SPA é servida em qualquer /admin/* (fallback para index.html). Assets com hash recebem cache imutável; index.html, no-cache.

D54. CodeMirror carregado sob demanda. O pacote principal fica em cerca de 130 KB gzip; o editor SQL (~145 KB gzip) só é baixado ao abrir sua página. As fontes Inter e JetBrains Mono vêm de @fontsource-variable (OFL), servidas pelo próprio binário.

D55. Painel sem ornamentos. Removidos padrões de modelos genéricos: cartões de estatísticas com ícones, gradientes, login brilhante, logo em quadrado colorido, etiquetas arredondadas e pontos em todo status, avatares com iniciais, esqueletos, notificações coloridas, ícone antes de cada título, subtítulos explicando o óbvio e a etiqueta "Postgres 17" (que era fixa, sem ler do banco). Critério: cor apenas para problemas (sem RLS, RLS sem políticas, erro, ação destrutiva); textos curtos. Inter/JetBrains Mono trocadas por IBM Plex Sans/Mono, menos comuns e com aparência de ferramenta técnica. Avisos de segurança permanecem, como texto simples.

Vários projetos por host

D56. Um host, N projetos isolados. Um Caddy compartilhado (o único a publicar 80/443) e, por projeto, um aplicativo e um Postgres em rede interna exclusiva. Um único projeto é um host com um projeto: não há dois modelos para manter. O registro (nelcota-host.json) é a fonte de verdade; o Caddyfile e a lista pública (shared/projects.json) são gerados a partir dele.

D57. Um Postgres por projeto. Papéis do Postgres valem para todo o servidor; em um Postgres compartilhado, projetos compartilhariam anon, authenticated, service_role e a senha de authenticator. O custo (~50 MB de RAM por projeto) oferece isolamento físico, backup/restauração e saída por projeto e atualizações independentes.

D58. Domínio próprio ou subdomínio do domínio-base. init api.shop.com deriva o nome "shop"; init --project blog usa blog.<base-domain>. Localmente: <project>.localhost (navegadores resolvem *.localhost para a máquina).

D59. Login único por transferência SSO, com opção de login por projeto. Cookies não atravessam domínios, então o painel de origem emite um JWT HS256 (segredo compartilhado do host, aud = projeto-alvo, 60 s, jti de uso único) que viaja no fragmento da URL e é trocado por uma sessão no destino. No modo por projeto, não há segredo compartilhado nem transferência. Apenas panel_login.rs escreve credenciais administrativas nos arquivos .env; trocar de modo regenera e reinicia os aplicativos.

D60. A lista de projetos é montada como pasta e relida em cada requisição. O aplicativo recebe shared/ apenas para leitura (bind de pasta, sem bind de arquivo, para tornar visível a renomeação atômica da CLI). Novos projetos aparecem no seletor sem reiniciar ninguém. O estado de cada projeto vem de GET /health pela rede nelcota_edge, que agora inclui a versão.

D61. Remoção sempre com backup final em archive/ antes de excluir contêineres e volumes; o site sai do Caddy com caddy reload (sem parar os outros).

Identidade e conta no painel

D62. A mascote é a identidade do painel, a única exceção a D55. Vem do atlas NelcotaScreenShare (src/assets/mascot/), recortada com poses ancoradas nos pés e no braço direito para não saltar ao mudar a expressão. Não é decoração porque reage ao estado da tela de login: acena ao abrir, segue o ponteiro com os olhos, fecha os olhos durante a digitação da senha e fica triste após um login recusado. Os olhos são uma camada vetorial sobre os do PNG (só a pupila se move); com prefers-reduced-motion, o olhar salta direto para o alvo.

D63. Barra lateral de altura completa. Projeto no topo (mesma altura e borda da barra superior), páginas no meio, conta embaixo; a barra superior cobre apenas o conteúdo. Fixada, ocupa 240px a partir de xl; recolhida, vira uma faixa de ícones que expande sobre o conteúdo. "Projetos" saiu do menu: o seletor de projetos já leva até lá.

D64. A foto do administrador fica no banco do projeto, no esquema nelcota (migração V4, nelcota.admin_avatar). O login administrativo só existe em variáveis de ambiente, então não havia onde guardar seus dados; o esquema interno não tem USAGE para nenhum papel da API. A foto é por projeto (cada um tem seu Postgres). O navegador recorta no centro e redimensiona para 256px em WebP; o servidor só aceita PNG, JPEG ou WebP reconhecidos pelos bytes iniciais, até 256 KB. A URL inclui a versão (?v=), então a imagem pode ter cache.

D65. nelcota-core recompila quando migrations/ muda. embed_migrations! lê a pasta sem avisar o compilador; um build.rs com rerun-if-changed evita compilações incrementais com a lista antiga.

D66. Recuperação de senha por e-mail, com o SMTP do projeto. Nelcota não executa servidor de e-mail: NELCOTA_SMTP_URL, NELCOTA_SMTP_FROM e NELCOTA_PASSWORD_RECOVERY_URL (as três ou nenhuma; com configuração incompleta, o servidor não inicia). O link vai para a página do aplicativo com o token no fragmento; tokens ficam em auth.one_time_tokens (migração V5) apenas como SHA-256, são válidos por 1 hora e uma vez, com no máximo um e-mail por minuto por conta. O envio ocorre em segundo plano para que o tempo de resposta não revele se a conta existe. Usar o link altera a senha, encerra sessões e marca o e-mail como confirmado. Enviado com lettre sobre rustls

  • ring, com as raízes Mozilla embutidas (webpki-roots): a imagem scratch não tem certificados do sistema e a compilação musl permanece sem OpenSSL. Isso trouxe duas licenças permissivas para deny.toml: CDLA-Permissive-2.0 (a lista de raízes, que são dados) e 0BSD (quoted_printable).

D67. DDL do painel se torna migração, registrada como já aplicada. Todo DDL aplicado pelo painel passa por apply.rs e é registrado em nelcota.panel_changes (migração V6), na mesma transação. "Gerar migração" reúne as pendentes em um arquivo e insere a linha correspondente em nelcota.user_migrations com o checksum do refinery (Migration::unapplied(..).checksum()): migrate não reaplica o arquivo e, com abort_divergent, recusa uma cópia editada. O texto gerado é armazenado (nelcota.panel_migrations) para baixar novamente byte por byte. A versão supera o maior número no banco, na pasta e entre exportações anteriores; a pasta vem de NELCOTA_MIGRATIONS_DIR ou da montagem /migrations no contêiner. O editor SQL fica de fora: pode executar qualquer coisa, inclusive DML, e separar alterações de esquema exigiria analisar o SQL.

D68. Código-fonte em inglês, painel traduzido. Código, identificadores, comentários, testes, mensagens do servidor (erros REST, de autenticação e da API administrativa), saída da CLI, e-mail de recuperação, README e esta documentação são em inglês. Apenas a interface do painel é traduzida: português (pt-BR) e inglês, por catálogos tipados por área (crates/admin/ui/src/lib/i18n/messages), nos quais o objeto em inglês é tipado a partir do português, então uma chave ausente falha na verificação de tipos. O painel escolhe o idioma do navegador na primeira visita e lembra a escolha do menu da conta. Erros da API administrativa contêm code e params ({"error", "code", "params"}), para o painel mostrá-los no idioma escolhido e usar o texto em inglês do servidor como alternativa (erros brutos do Postgres, por exemplo). Migrações V1–V6 mantêm nomes de arquivo e comentários em português: refinery calcula checksums do nome e conteúdo, e editá-los faria toda instalação existente se recusar a iniciar.

D69. Bibliotecas em vez de quatro partes escritas à mão. Uma revisão do que o código reimplementava manteve as partes deliberadas (construtor SQL de D29, OpenAPI de D30, parser no estilo PostgREST, geração TypeScript, CSV, geração de segredos, identificação de imagens) e substituiu quatro:

  • Limite de requisições: GCRA por chave de governor em vez de janela fixa. O orçamento se recompõe continuamente, então um cliente não pode mais gastar quase o dobro do limite na virada do minuto, e Retry-After é a espera real. Ainda em memória por processo; apenas os recursos std e dashmap.
  • Cookie de sessão: o crate cookie analisa o cabeçalho Cookie e constrói Set-Cookie em um lugar, em vez de split(';') e format!. Sem recursos de assinatura ou criptografia: o valor é um token opaco.
  • Sonda de saúde: cliente de hyper-util em vez de requisição HTTP/1.1 e separação de resposta escritas à mão; um corpo em chunks não o quebra mais. hyper já estava na árvore via axum.
  • Aspas de identificadores: postgres_protocol::escape::escape_identifier, já uma dependência. Literais de string no DDL gerado mantêm a duplicação manual de '': escape_literal emitiria E'...' para barras invertidas e mudaria o texto das migrações geradas pelo painel.

D70. or=/and= como árvores lógicas do PostgREST. Os filtros da requisição formam uma árvore (Condition): folhas são os filtros existentes e grupos unem seus itens com OR/AND, opcionalmente negados; o nível superior continua combinado com AND, então URLs existentes mantêm o significado. Cada grupo é renderizado como expressão entre parênteses, para a precedência nunca depender do entorno, e folhas mantêm as regras D29 (colunas do catálogo, identificadores entre aspas, valores parametrizados). A profundidade é limitada a 8 e uma árvore a 100 filtros, limitando o parser recursivo em entradas hostis. Como no PostgREST, or/and são chaves reservadas de consulta.

D71. Upsert por Prefer: resolution e on_conflict, como no PostgREST. Cada grupo INSERT (D33) recebe ON CONFLICT (key): DO UPDATE SET para as colunas enviadas pelo objeto, exceto a chave, ou DO NOTHING; uma mesclagem sem nada para atualizar usa DO NOTHING. A chave é a primária ou on_conflict, cujas colunas são verificadas no catálogo; decidir se formam chave única fica com o Postgres, e seus erros 42P10 e 21000 agora mapeiam para 400 em vez de 500. RLS não precisa de tratamento especial: o Postgres aplica a política INSERT à nova linha e a UPDATE à existente.

D72. Relacionamentos embutidos por chaves estrangeiras, um nível, como subconsultas correlacionadas. select= aceita [alias:]table[!hint](columns). O relacionamento vem apenas das chaves estrangeiras do catálogo dentro do esquema exposto, nas duas direções: a chave desta tabela dá um objeto (to_json de uma subconsulta que corresponde a no máximo uma linha, pois a chave referencia uma única), a chave da outra dá um array (json_agg). Cada relacionamento embutido é uma subconsulta na lista select, então executa na mesma transação com o papel da requisição e a RLS da tabela relacionada o filtra como uma leitura direta; não há lógica de permissão separada. Ligações ambíguas pedem uma indicação (coluna ou nome da restrição); aninhamento e autorreferências são recusados por enquanto. O parser do painel continua recusando relacionamentos embutidos (parse_request); a API usa parse_request_with_relations.

D73. Relacionamentos embutidos aninhados e filtros por relacionamento, como no PostgREST. A lista de colunas de um relacionamento é um select completo, permitindo aninhamento de no máximo 4 níveis (cada nível é mais uma subconsulta correlacionada por linha pai; o teto limita o que uma única URL pode acumular). Parâmetros prefixados pela chave do relacionamento (items.qty=gt.1, orders.items.order=...) vão para ele: filtros e árvores or/and se juntam à chave estrangeira no WHERE da subconsulta, e order/limit/offset paginam o array embutido (recusados para linha única). Restringem apenas linhas embutidas; linhas pai permanecem, com null ou []. select é analisado primeiro, então a ordem dos parâmetros na URL não importa. Filtros agora contêm o tipo da coluna, para o mesmo construtor SQL atender a qualquer alias de linha. Subconsultas aninhadas recebem aliases pelo caminho (_e0_1) para um filho nunca ocultar seu pai. Autorreferências continuam recusadas, e filtrar pais pelos relacionamentos embutidos (!inner do PostgREST) não está implementado.

D74. PITR com pgBackRest, dentro do contêiner Postgres do projeto. pgBackRest em vez de WAL-G: Alpine o empacota (2.58), então a imagem permanece postgres:17-alpine mais um apk add. WAL-G só distribui binários glibc, e mudar para Debian alteraria a libc e, com ela, a ordenação de texto, invalidando silenciosamente os índices de projetos existentes. A imagem é construída localmente de postgres/Dockerfile (sem segunda imagem para publicar). Configurações trafegam como variáveis PGBACKREST_* de pgbackrest.env (modo 600) por env_file com required: false, então nenhum arquivo de configuração precisa ser legível pelo usuário postgres do contêiner e "PITR ativado" significa apenas esse arquivo existir; o arquivamento em si usa ALTER SYSTEM (archive_mode, archive_command, archive_timeout = 60s), sem interferência das opções -c do perfil. O Postgres recebe uma rede egress por projeto: a internal não tem rota de saída, e nada pode entrar por nenhuma delas. Backups-base usam o backup --all diário existente (completo aos domingos, diferencial nos demais dias, dois completos mantidos). Uma restauração recria o contêiner Postgres, para sua contagem de reinícios distinguir uma recuperação com falha (um alvo além do arquivo faz o Postgres encerrar), e termina com backup completo: a nova linha do tempo se ramifica antes de qualquer backup posterior na antiga, do qual o pgBackRest recusaria restaurar.

D75. Hosts sem Docker: um projeto, unidades systemd, pacotes apt. init --runtime systemd (Debian/Ubuntu, como root) obtém Postgres 17 e pgBackRest do PGDG e Caddy de seu repositório: versões correspondem às dos hosts Docker e atualizações de segurança vêm com apt upgrade. Um projeto por máquina: N projetos isolados significariam N clusters em N portas e N usuários do sistema, e esse isolamento já é fornecido pelo Docker. O ambiente de execução fica no registro ("runtime", Docker quando ausente), e toda operação passa por Project, que transforma "iniciar aplicativo", "executar como postgres", "Postgres está saudável" em docker compose ou systemctl/runuser/journalctl; ops e pitr não sabem mais qual é. O aplicativo executa uma cópia do binário (/usr/local/lib/nelcota/nelcota) para upgrade poder trocá-lo e manter o anterior para reversão, em uma unidade com DynamicUser, ProtectSystem=strict, sem capabilities e apenas porta loopback. Comandos que precisam da configuração do aplicativo (migrate, types, token) executam esse binário como processo filho com o .env do projeto, em vez de definir variáveis no próprio processo da CLI. pgBackRest lê as mesmas configurações do Docker, escritas como /etc/pgbackrest/pgbackrest.conf (archive_command executa sob a unidade Postgres, que não recebe o .env).

D76. A imagem é ghcr.io/nelcota/nelcota-server. O nome correspondente ao repositório, ghcr.io/hanielcota/nelcota, é um pacote de um projeto anterior (agora NelcotaScreenShare) cuja produção ainda usa a tag latest. Publicar lá sobrescreveria essa tag, e excluí-la quebraria a implantação, então este projeto publica sob outro nome. A imagem de publicação contém os binários musl publicados copiados para scratch (deploy/release.Dockerfile), vinculados a este repositório pelo rótulo org.opencontainers.image.source.

Armazenamento

D77. Metadados no Postgres sob RLS, bytes em armazenamento de objetos. Um esquema storage contém storage.buckets e storage.objects (bucket, nome, proprietário DEFAULT auth.uid(), tamanho, tipo, etag). Quem pode ler, escrever ou excluir um arquivo é decidido por políticas RLS em storage.objects, escritas em SQL simples como qualquer política; o servidor nunca decide isso. Os bytes ficam por trás do crate object_store (Apache Arrow) com dois backends: disco local (padrão, sem configuração após nelcota up) e qualquer serviço compatível com S3 (recomendado para produção: R2, B2, AWS...). object_store é usado com aws-base + ring, nunca aws-lc-rs, pelo mesmo motivo de D16. Sem servidor de objetos embutido: Garage em um nó mantém arquivos no mesmo disco que um diretório simples, adicionando mais um serviço para executar, e a edição comunitária do MinIO entrou em manutenção em 2025. As rotas seguem o formato do Supabase Storage (/storage/v1/object/{bucket}/{path}), sem prometer compatibilidade com seu cliente; as palavras das rotas (public, sign, list) não podem ser nomes de buckets.

D78. Uploads passam pelo servidor em streaming; chaves são versionadas. O cliente envia o arquivo ao Nelcota, que o transmite ao armazenamento: um endpoint, o Bearer usual, sem CORS em segunda origem, com tamanho, tipo e cota verificados no mesmo fluxo. Antes de receber os bytes, um INSERT com rollback sob o papel do chamador verifica a política, então um chamador sem permissão não pode encher o disco. Os bytes vão para {bucket}/{version} (UUIDv7), e não para o nome do objeto; apenas após concluir o upload uma transação curta insere ou atualiza a linha sob RLS. Não manter conexão durante o fluxo de bytes impede clientes lentos de esgotar o pool; chaves versionadas tornam a substituição atômica (leitores veem o arquivo antigo ou o novo, nunca uma parte) e impedem um upload com falha de destruir algo. Excluir ou substituir remove os bytes antigos logo após o commit: excluir significa que o arquivo se foi, então uma restauração do banco não traz arquivos excluídos de volta. Um coletor periódico remove bytes sem referência de linha (deixados por falha no meio do upload) após um dia, para nunca disputar com upload em andamento. Um upload simples precisa apenas de política INSERT; substituir e excluir também precisam de SELECT, como UPDATE/DELETE com WHERE no Postgres. Rotas de armazenamento executam fora do timeout de requisição e da camada gzip.

D79. Arquivos de usuários são servidos como conteúdo inerte. Toda resposta de objeto inclui X-Content-Type-Options: nosniff e Content-Security-Policy: sandbox; apenas imagens rasterizadas, áudio, vídeo e texto simples são servidos como inline, todo o restante (HTML, SVG, PDF...) como attachment. O tipo é identificado pelos primeiros bytes, sem usar o valor do cliente. Caminhos rejeitam .., segmentos vazios, caracteres de controle e nomes longos demais e são normalizados para Unicode NFC. URLs assinadas são JWTs de curta duração assinados com as chaves do projeto e vinculados a um bucket e caminho. No backend S3, um download é 302 para URL pré-assinada no domínio do provedor; em disco, o servidor transmite com Range e ETag. NELCOTA_STORAGE_PUBLIC_URL pode mover arquivos públicos para uma origem própria (por exemplo files.shop.com).

D80. O armazenamento não pode derrubar o banco. Em uma VPS única, arquivos e Postgres compartilham disco. Todo upload tem teto de tamanho (por bucket e global NELCOTA_STORAGE_MAX_FILE_SIZE, 50 MiB por padrão); uma cota total opcional cobre todos os buckets; e no backend em disco um upload é recusado quando o espaço livre ficaria abaixo de NELCOTA_STORAGE_MIN_FREE_BYTES (1 GiB por padrão). Uploads em andamento reservam capacidade, e a publicação verifica o total resultante sob um bloqueio transacional do banco entre instâncias. Substituições descontam seu tamanho lógico anterior ao reservar espaço para a nova versão. Cotas por usuário ficam com as políticas, que veem a linha final (com tamanho) na inserção. Transformações de imagem e uploads retomáveis (TUS) ficam de fora: decodificar imagens não confiáveis é uma fonte constante de CVEs.

D81. Novos projetos armazenam arquivos no disco; backups os espelham (o espelho compartilhado de backup é substituído por D82). nelcota init escreve NELCOTA_STORAGE_BACKEND=disk: no Docker, o aplicativo monta projects/<name>/storage (pertencente ao usuário 65532 da imagem); no systemd, o StateDirectory da unidade. O armazenamento funciona logo após nelcota up, e mover para S3 exige duas linhas no .env. Um arquivo no mesmo disco do banco é perdido junto dele, então backup --upload também executa aws s3 sync --delete desse diretório para <bucket>/<project>/storage/: chaves nunca mudam após a escrita (D78), então cada execução só envia arquivos novos, e restore --files os traz de volta. O Caddy deixa de comprimir /storage/*, o que quebraria intervalos de bytes. Projetos criados antes do armazenamento o mantêm desativado até adicionar as duas linhas e o volume (docs/storage.md).

Confiabilidade do painel e dos backups

D82. Backups de disco capturam arquivos imutáveis junto de cada dump. Um espelho compartilhado perde versões antigas quando uploads posteriores as substituem ou excluem, então não pode restaurar arquivos referenciados por um dump histórico do banco. Cada dump agora tem seu próprio diretório .files/ e manifesto SHA-256. Um snapshot exportado do Postgres e bloqueio SHARE em storage.objects mantêm as versões copiadas e metadados do dump consistentes. Envie os arquivos antes de publicar o dump; retenha e arquive o par junto. A restauração valida o manifesto antes de parar o aplicativo e copia versões imutáveis antes de restaurar o banco. Dumps antigos continuam utilizáveis para restaurações apenas do banco (docs/backup.md).

D83. O painel valida contratos de comunicação gerados do Rust. ts-rs e schemars exportam TypeScript e JSON Schema dos DTOs do servidor; Ajv emite validadores independentes para o navegador, então a CSP existente não precisa de geração de código em tempo de execução. Verificações de divergência executam na CI. Valores de células do banco continuam texto ou NULL (D48). Estado de recursos e filas de upload cuidam de cancelamento, ordenação de resultados e destino capturado do upload, com testes unitários e de regressão no navegador.

D84. Limites de exibição SQL restringem memória sem alterar a semântica do lote. Conexões dedicadas do editor (D47) permitem no máximo quatro execuções com timeout de instrução de 30 segundos. O cliente retém até 1000 linhas por resultado, 8 MiB de valores e 32 resultados exibidos, enquanto o servidor consome o restante do lote para instruções posteriores ainda executarem. Descartar uma requisição envia cancelamento ao Postgres e fecha sua conexão. DDL informa applied separado de catalog_pending: uma atualização com falha após COMMIT repete com espera progressiva em vez de pedir ao usuário para repetir uma mutação já confirmada.

D85. Módulos seguem motivos independentes de mudança. Autenticação compartilha operações de sessão/token e transações do papel de autenticação independentemente de seus handlers HTTP. A composição de rotas do painel não cuida de cookies, cabeçalhos de resposta ou entrega de assets. Operações de armazenamento aceitam stream de bytes e opções explícitas e retornam resultados tipados; rotas públicas e painel compartilham o adaptador HTTP. Ciclo de vida da CLI, sequência de backup, transporte remoto e atualizações de versão têm responsáveis separados. O módulo de linhas do editor de tabelas cuida de carregamento e reconciliação de escritas, enquanto a página cuida de navegação e apresentação. Garantias existentes de transação, RLS e snapshot continuam coordenadas dentro de seus módulos de operação (docs/architecture.md).

D86. Interfaces de operação cuidam de ciclos de vida completos. Fluxos de recuperação e migração retornam dados sem extração HTTP nem construção de respostas. Suas alterações atômicas no banco permanecem sob um único responsável pela transação. O executor SQL cuida de conexão, cancelamento e limites de exibição por um método de execução; a rota escolhe a resposta HTTP. A execução REST retorna JSON já construído pelo PostgreSQL e metadados de resposta, preservando RLS e ordem de erros. Modelo do catálogo, introspecção e workers de atualização permanecem internos à fachada existente. Módulos SQL e de armazenamento do painel aceitam adaptadores de transporte e cuidam de cancelamento, destinos capturados e atualizações após mutações. Provisionamento nativo e adaptadores de configuração/processos PITR são separados da sequência de restauração, sem novo ambiente de implantação ou contrato de comandos.

D87. A responsabilidade pelo recurso determina a localização do código. Páginas, estado, adaptadores, componentes e testes unitários do painel ficam junto do recurso. A estrutura do aplicativo e as funções compartilhadas de esquema/apresentação têm locais nomeados; contratos gerados de comunicação continuam sendo a referência oficial. Fachadas administrativas do backend agrupam handlers de tabelas, políticas e contas e exportam explicitamente sua interface. Testes de integração compartilham funções auxiliares de requisição do painel e mantêm cenários de recursos em uma suíte administrativa. Isso muda a navegação do código sem introduzir outro ambiente de execução, camada de crates, contrato HTTP ou responsável por transações (docs/architecture.md).

D88. Fachadas expõem operações completas no monólito modular. Os crates de recursos existentes e o binário único permanecem como arquitetura. Leituras e mutações de buckets agora têm um único responsável no armazenamento, compartilhado pela API pública e pelo painel, com registros e erros tipados. Adaptadores HTTP mantêm seus contratos de comunicação e normalização de entradas; PostgreSQL ainda aplica privilégios, RLS e impede exclusão de buckets não vazios pela chave estrangeira. Operações de contas do painel cuidam de validação, SQL e transações de senha/sessão independentemente do HTTP. Reutilize os adaptadores existentes de disco/S3 e e-mail onde as implementações variam; não adicione repositório genérico, contêiner de dependências ou camada extra de crates. Testes de integração exercitam políticas de buckets pela interface de operações e comprovam que uma revogação de sessão com falha reverte a troca de senha.

D89. Confirmação de cadastro e links mágicos reutilizam o link de recuperação. Duas páginas opcionais, NELCOTA_EMAIL_CONFIRMATION_URL e NELCOTA_MAGIC_LINK_URL, precisam do SMTP de D66 e ativam um fluxo cada; sem elas nada muda. Seus tokens compartilham auth.one_time_tokens (V9 amplia o CHECK kind para signup e magiclink), token no fragmento da URL, um e-mail por minuto por conta e tipo e envio em segundo plano. Um token só funciona como o tipo para o qual foi emitido. Com confirmação ativada, o cadastro responde 201 {user} sem sessão, e o login por senha responde 400 email_not_confirmed (apenas após senha correta, sem revelar nada) até abrir um link de cadastro, mágico ou de recuperação; POST /auth/v1/resend {type: "signup"} envia um novo link apenas para contas não confirmadas. Ativar confirmação em um projeto com contas não confirmadas também as bloqueia: confirmam por resend ou link mágico, ou o operador executa UPDATE auth.users SET email_confirmed_at = now(). Um link mágico só autentica contas existentes: criar contas a partir de solicitação de e-mail não autenticada permitiria encher auth.users e reservar endereços antes do cadastro dos proprietários. Validades: recuperação 1 hora, confirmação 24 horas, link mágico 15 minutos (autentica sozinho). O módulo links cuida do ciclo do link (armazenamento do token, texto do e-mail, envio, login por link); recovery mantém apenas a troca de senha, confirmation a política de cadastro e verify o encaminhamento por type.

D90. Contas do painel são consideradas confirmadas. Uma conta criada pelo administrador é marcada como confirmada imediatamente: o operador avaliza o endereço, e com confirmação de cadastro ativada ela ficaria bloqueada até um link mágico ou resend. A página de usuários marca contas não confirmadas e oferece "Confirmar e-mail" (POST /admin/api/users/{id}/confirm, idempotente), substituindo o UPDATE SQL de D89 para projetos que ativam confirmação com contas antigas. Não envia e-mail: o painel não tem serviço de envio e um fluxo de convites é uma decisão separada.

Provedores de login

D91. Login OAuth/OIDC com PKCE nas duas etapas e vinculação por e-mail verificado. Google (userinfo do OpenID Connect) e GitHub (perfil REST mais lista de e-mails, o único lugar que informa se um endereço foi verificado) ficam por trás de um tipo Provider, cada um em seu módulo; outro provedor é outro módulo. O aplicativo começa em GET /auth/v1/authorize com seu desafio S256; Nelcota mantém o login em auth.flow_states (V10) com um verificador PKCE separado para o provedor, resgata o código do provedor de servidor para servidor e envia a pessoa para redirect_to?code=.... O aplicativo troca esse código e seu verificador por sessão em POST /auth/v1/token?grant_type=pkce, então um código lido de URL, log ou Referer é inútil sozinho; um código é tentado uma vez. redirect_to deve estar sob uma das NELCOTA_OAUTH_REDIRECT_URLS (mesma origem, caminho abaixo), então /authorize não é um redirecionamento aberto; o callback é NELCOTA_API_URL/auth/v1/callback. Uma conta de provedor é identificada por seu ID (auth.identities, único por provedor), nunca pelo e-mail. Um primeiro login vincula uma conta existente apenas quando o provedor verificou o e-mail: uma conta confirmada é vinculada como está; uma não confirmada é assumida pelo proprietário verificado, perdendo senha, sessões e links que outra pessoa possa ter criado com o endereço (tomada de conta pré-cadastrada); um e-mail não verificado correspondente a uma conta é recusado (email_conflict). Novas contas não recebem senha e são confirmadas quando o provedor verificou o e-mail. Recusas e erros do provedor voltam ao aplicativo como ?error=<code>, e um state desconhecido gera página de erro, pois não há destino confiável para retornar. O cliente HTTPS é o reqwest já presente na árvore para S3, sobre rustls com ring (D16), com timeout de 10 s, sem redirecionamentos e corpos de resposta limitados.

Painel

D92. Código recebe cores de sintaxe, a segunda exceção a D55. O editor SQL colore palavras-chave, chamadas de funções, tipos, strings, números e comentários; nomes de tabelas e colunas mantêm a cor do texto. A cor transmite significado (é como as pessoas leem código) em vez de decorar a página. A paleta segue os temas de sintaxe claro e escuro do GitHub, referência testada e legível, como tokens --syntax-* junto das outras cores de tema. A gramática SQL não marca chamadas de função, então um pequeno plugin de visualização colore um identificador imediatamente antes de ( e uma palavra-chave embutida ligada a ele (count(), mas não palavras-chave que abrem uma cláusula ou lista (in (, exists(). Apenas o editor carregado sob demanda muda (D54); blocos SQL somente leitura continuam simples.

D93. Linguagem simples primeiro, técnica sob demanda; a visão geral orienta. Regras de acesso são lidas pelo significado para os usuários do aplicativo: uma tabela é Protegida, Bloqueada (proteção ativada, sem regras) ou Desprotegida; grants viram "Somente leitura" ou "Leitura e escrita" para Visitantes e Usuários autenticados; uma política é uma frase ("Cada usuário só vê e altera suas próprias linhas") quando corresponde a um formato conhecido (leitura pública ou autenticada, proprietário por auth.uid()), e "Regra personalizada" com SQL visível nos demais casos. Um seletor "Detalhes técnicos", lembrado por navegador, traz de volta RLS, papéis, grants e SQL. A visão geral começa pelo que precisa de ação (tabelas desprotegidas e bloqueadas, cada uma ligada às políticas) e lista de próximos passos marcada apenas pelo estado do projeto: primeira tabela, todas as tabelas da API protegidas, um usuário e primeiro login pela API (signed_in_users), indicando aplicativo conectado. Contagens se reduzem a uma linha. Navegação e cor de destaque permanecem (D55, D63).

D94. O editor SQL pode executar como visitante ou usuário autenticado. Por padrão, ainda executa como proprietário do banco, sem RLS, indicado na barra de ferramentas pelo aviso âmbar "Acesso total". O aviso também é o seletor: "Visitante" executa como anon, e "Usuário autenticado..." executa como authenticated com as claims de uma conta real escolhida por e-mail (sub, email), então auth.uid(), auth.jwt(), grants e políticas se comportam como para essa pessoa pela API. A conexão do editor é dedicada a uma execução, então o executor define papel e request.jwt.claims para a sessão antes do SQL, como a API faz por requisição. É uma forma de testar regras, não um limite de segurança: o administrador sempre pode executar qualquer coisa como proprietário, e o SQL pode até executar RESET ROLE. A escolha não persiste, então recarregar retorna ao proprietário.

SDK do cliente

D95. Nelcota distribui seu próprio cliente TypeScript, uma camada fina sobre HTTP. @nelcota/client fica em sdk/typescript/ para seus testes de contrato executarem na mesma CI com o binário real; não usa cliente BaaS de terceiros. Cada método é uma requisição HTTP sem cache oculto, e o acesso permanece com a RLS: o cliente nunca decide permissões. Não tem dependências de execução (apenas fetch, URL, Web Crypto, AbortSignal, Web Locks e BroadcastChannel), então executa em navegadores, Node 22+, Deno, Bun e ambientes de borda, é ESM com exportações por subcaminho e sem efeitos colaterais e limita a cadeia de dependências ao compilador TypeScript. Resultados são valores ({ data, error }) que contêm o code do servidor intacto; apenas erros de programação lançam exceções. Entrada não confiável nunca se torna sintaxe: identificadores são validados, valores de grupos or/and e listas in sempre ficam entre aspas, caminhos de armazenamento são codificados por segmento com .. recusado e um token service_role no navegador é recusado porque ignora RLS. Há uma única renovação em andamento por processo e, nos navegadores, serialização entre abas por Web Lock, pois a rotação encerra uma sessão cujo token foi reutilizado (D23). Novas tentativas cobrem apenas requisições idempotentes em 429/503 e respeitam Retry-After. Tipos vêm de nelcota types, que agora também emite Relationships de cada tabela para tipar uma string select até seus relacionamentos embutidos. O servidor expõe Retry-After, Preference-Applied e Last-Modified por CORS para o navegador poder lê-los. O SDK segue seu próprio versionamento semântico e informa a versão mínima de servidor necessária.

Ferramentas

D96. As ferramentas seguem as versões estáveis mais recentes. A MSRV muda de 1.88 (D19) para 1.99, a versão estável que a CI, a imagem Docker e a compilação de publicação já usam, para cargo update escolher os crates mais recentes em vez dos mais recentes que compilam em um compilador antigo. TypeScript é 7, o compilador nativo: os projetos TypeScript simples do SDK e do painel o usam na verificação. svelte-check ainda precisa da API de compilador TypeScript 6 (TS 7 ainda não tem API JS estável), então o painel o mantém como typescript, de @typescript/typescript6, enquanto tsc vem de typescript-7 e os scripts o chamam pelo caminho, pois ambos os pacotes distribuem um binário tsc. Remova o pacote TS 6 quando svelte-check suportar 7. Atualizações ainda aguardam sete dias após a publicação (intervalo de espera do Dependabot), então "mais recente" significa a versão mais recente disponível há uma semana.

Conectar aplicativos e acesso

D97. O painel conecta aplicativos e explica recusas. A página API começa com o SDK: instalação, download de database.ts gerado pelo mesmo código de nelcota types e exemplos tipados para a tabela escolhida. Três ferramentas respondem "por que meu aplicativo não vê isso?": a grade pode ser lida como visitante ou usuário (papel e claims do executor D94, em transação com rollback, somente leitura); cada bucket lista políticas de storage.objects que o nomeiam (ou que não nomeiam nenhum bucket) e adiciona novas a partir de cinco modelos no servidor, para nenhum texto do navegador se tornar SQL e o painel reconhecê-las em palavras simples; e o servidor registra chamadas da API respondidas com 401, 403 ou 429 em buffer circular de 100 entradas para a página de políticas. Esse log fica em memória intencionalmente: nenhuma escrita por requisição recusada, nada para reter ou remover e por processo, correspondendo a um painel por projeto. Mantém o caminho, sem query string ou token, e lê o papel verificando novamente o token apenas nas chamadas recusadas. A página de login é somente leitura porque as configurações vêm do ambiente. Páginas fora do caminho principal (políticas, bucket, login) carregam sob demanda para o pacote principal permanecer no orçamento de 900 kB.

Redesign do painel

D98. Aparência de dashboard com tráfego real, substituindo a contenção de D55 e a barra lateral de D63. O painel segue um dashboard de referência mantido pelo usuário: seus tokens (tons oklch de página e cartão, verde vivo para ações, raio-base de 0.75rem que produz cartões de 26.4px), barra superior de três colunas com páginas em botões arredondados curtos e indicador de conta, páginas de largura completa com margens de 32px, títulos internos de 24px e um grande na visão geral, cartões sem borda, buscas arredondadas e tabelas mais leves. A visão geral espelha o layout da referência: um seletor de período para a página, tráfego do período como destaque com medidores, cartões estatísticos ao lado do gráfico e três cartões abaixo (linhas por tabela, chamadas bloqueadas recentemente, o que precisa de atenção). Gráficos precisam de dados ao longo do tempo, então o servidor conta requisições da API por minuto (recusadas, erros de servidor, histograma de latência para p95) por 24 horas em memória, junto do log D97: nenhuma escrita por requisição, desaparece ao reiniciar, como a visão geral informa. Como na referência, requisições são uma área verde e recusas uma cinza; uma série cinza sozinha falharia em verificação de visão de cores, então valores continuam acessíveis por filtros de série, dica, cursor de teclado (controle deslizante para tecnologia assistiva) e visualização em tabela. A mascote e as cores de sintaxe (D62, D92) permanecem; linguagem simples primeiro (D93) permanece.

Revisões posteriores definiram as regras seguidas por novas telas:

  • Superfícies, da mais escura à mais clara no modo escuro: página (grafite, um tom mais claro que a referência a pedido do usuário), cartão (rounded-3xl), área interna (rounded-2xl dentro do cartão: código, blocos, linhas, estados vazios), campo (entradas, um tom acima da área interna). No modo claro, a página é cinza, cartões e campos brancos, áreas internas cinza-claro. Caixas são preenchidas, sem contorno; borda tracejada marca apenas áreas de arraste e blocos de "adicionar", e preenchimento com tom (destrutivo ou aviso a 10%) marca perigo e alertas.
  • Barra superior: indicador do projeto (mascote, projeto, ambiente, ponto informando se o servidor responde) e indicador de conta equilibram os seis botões arredondados centrais; a busca é um botão redondo. Tabelas, SQL e Migrações compartilham o botão Banco de dados, com abas entre elas dentro das páginas.
  • Grade da página: páginas têm largura máxima de 1440px. Números abrem páginas que os possuem, nas mesmas faixas das colunas abaixo; colunas laterais têm 24rem. Grades começam pela largura do contêiner, para nada rolar lateralmente em celulares.
  • Contraste: tons de texto são verificados contra toda superfície nos dois temas (4.5:1 ou mais para texto de corpo), sem depender de estimativa visual.

Bibliotecas consolidadas para os protocolos e a infraestrutura do painel

O painel usa TanStack Query para compartilhar leituras simultâneas e permitir janelas opcionais de validade dos dados. Recargas explícitas continuam consultando o servidor. Alterações invalidam as leituras; trocas de sessão limpam os resultados em cache e os pendentes, e cada chamada recebe uma cópia independente para que editar uma linha não altere dados compartilhados. TanStack Table gerencia a seleção de linhas e a visibilidade das colunas usando chaves primárias estáveis; a edição por teclado, a ordenação no servidor e as permissões existentes continuam fazendo parte da grade.

Uppy e XHRUpload cuidam dos uploads de arquivos com corpo bruto e do progresso. As tentativas automáticas estão desativadas porque a conexão pode falhar depois que o upload já foi salvo. O destino capturado no início e as decisões sobre conflitos continuam na fila de uploads. Shiki substitui o realce por expressões regulares por gramáticas carregadas sob demanda e seu mecanismo JavaScript. Os tokens continuam como texto escapado pelo Svelte; o navegador dispensa eval em execução, WebAssembly e APIs do Node.

Scalar apresenta o catálogo OpenAPI anônimo em um frame da mesma origem, isolando seus estilos e a navegação por fragmentos do painel. Seus arquivos estão incluídos no binário Rust; fontes externas, telemetria, o agente e o proxy de requisições ficam desativados. Somente esse documento de referência permite frames da mesma origem. O restante do painel mantém suas restrições de frames. Scalar só é carregado quando a referência é aberta.

A biblioteca Rust oauth2 monta URLs de autorização, PKCE S256 e trocas de tokens; openidconnect interpreta as informações tipadas do usuário fornecidas pelo Google. Ambas usam o transporte reqwest existente com respostas limitadas, sem adicionar outra pilha HTTP. Nelcota mantém suas políticas de estado de uso único, vinculação de contas e sessões. Essas dependências dispensam um servidor JavaScript: Rust serve o painel compilado e o backend.

D99. Conversões do cliente preservam a identidade dos valores do Postgres. Números JSON continuam numéricos quando a conversão preserva o valor decimal. O SDK TypeScript mantém como strings os inteiros fora da faixa segura e decimais que perderiam dígitos, inclusive em valores JSON aninhados e resultados de RPC. Tipos numéricos gerados que podem exceder a precisão do JavaScript usam number | string, para que o código trate ambas as formas e reutilize os valores exatos em filtros e escritas. Isso preserva o protocolo HTTP existente e não exige dependência de runtime nem suporte ao texto original no reviver de JSON. Elementos de arrays PostgreSQL podem ser nulos mesmo em colunas NOT NULL; por isso arrays escalares e de enums gerados usam (T | null)[]. A nulabilidade da coluna continua separada. Gere novamente os tipos do esquema após atualizar o SDK.

D100. O trabalho de banco do painel tem limites definidos e páginas de storage indexadas. As prévias de tabelas reutilizam uma conexão administrativa e revertem o papel do chamador antes de ler os metadados como dono. As contagens de tabelas pequenas examinam no máximo 10.001 linhas com limite de 250 ms por comando. Quando uma tabela cresce além da estimativa pequena, seu total fica desconhecido até que as estatísticas forneçam uma estimativa maior.

As exportações de tabelas usam duas vagas de conexões dedicadas, compartilhadas por todas as sessões do painel, com limite de 30 segundos por comando e duração de dois minutos para a resposta. Descartar uma resposta cancela sua consulta. A conclusão verifica o prazo diretamente e disputa atomicamente com a interrupção, para que um watchdog atrasado não informe que uma exportação fora do prazo está completa. A vaga continua ocupada durante o cancelamento com prazo limitado.

As páginas de diretórios do storage buscam por uma chave de bytes gerada a partir do caminho pai e pulam a subárvore de cada pasta retornada. Os arquivos diretamente na pasta usam um índice ordenado por hash de escopo gerado e nome; verificações exatas de bucket e caminho pai rejeitam colisões. Cada busca continua sujeita ao RLS de objetos do chamador. As transações de listagem desabilitam varreduras por bitmap localmente para favorecer buscas ordenadas antes que as estatísticas sejam atualizadas. As funções puras de chave permitem execução pública para preservar os escritores SQL customizados; seus valores gerados não concedem privilégios adicionais de acesso.

A V14 indexa a ordenação estável de usuários por data de criação e ID. A V15 calcula chaves de diretório e hashes de escopo armazenados e cria seus índices sob o lock de migração. Instalações grandes precisam de tempo para essa reescrita única dos metadados. Os testes de upgrade verificam linhas originais, objetos de esquema, políticas e arquivos, incluindo caminhos SQL antigos cujos nomes multibyte excedem o limite de bytes do HTTP.

D101. Atualizações usam um plano de controle nativo autorizado separadamente. O mesmo binário executa uma função HTTP/UI sem privilégios e um worker IPC root restrito, em uma origem de gestão independente do tráfego dos projetos. Credenciais de projeto/SSO/service-role não concedem autoridade sobre o host; a autorização pertencente a root vincula o escopo do operador à identidade de uma instância de projeto. Apenas artefatos oficiais, prontos, com digest e atestação, de versões estáveis são elegíveis. A capacidade de deployment é dona de recibos tipados, leases compartilhados do host e transições duráveis; adaptadores CLI cuidam dos efeitos do runtime e admin do HTTP/UI incorporado. Confirmar tráfego público e reiniciar uma versão restaurada são fronteiras irreversíveis dos dados. Rotas/efeitos em segundo plano do candidato ficam bloqueados pela identidade da operação. A instalação em produção começa desativada até a evidência de falhas/recuperação externa em VPS com o binário exato passar. Veja o guia exclusivo do repositório em docs/panel-updates.md e G03 em docs/goals/panel-updates-and-recovery.md. Eles não são guias registrados do painel.

Pendências conhecidas

  • Filtrar linhas pai pelos relacionamentos embutidos (!inner) e relacionamentos autorreferentes.
  • nelcota.com/install deve redirecionar para https://raw.githubusercontent.com/Nelcota/Nelcota/main/scripts/install.sh.