--- title: "RSS Social — leitor social da web aberta" subtitle: "Proposta revisada: implementação limpa em Go" date: 2026-07-23 summary: "Proposta de um leitor social da web aberta, implementado em Go, com interoperabilidade por RSS, modelo de dados convergente, moderação e operação em binário único." tags: [pesquisa, rss-social, rss, textcasting, go, web-aberta, self-hosting] flowerColor: "#72d6c9" status: rascunho para análise — nada implementado substitui: "2026-07-22-pesquisa-026-ideia-afluente-plataforma-social-feeds.md" --- # RSS Social > Cada site mantém sua origem. A conversa corre em comum. Documento de proposta. Não é plano de implementação, não é spec técnica final. É a ideia refinada até o ponto em que dá para discordar dela com precisão. --- ## 0. O que mudou em relação ao documento original O documento anterior (*Afluente*, 22/07) foi escrito a partir da leitura do README, da inspeção visual do app público e das especificações. Esta revisão foi escrita depois de **clonar e ler o código** do [RSC](https://github.com/rmdes/rsc) e do [rss.chat](https://github.com/scripting/rss.chat), incluindo os 120 documentos de spec/plano/review do primeiro. Sete mudanças de rumo: | # | Antes | Agora | Por quê | |---|---|---|---| | 1 | Fork responsável do RSC | **Implementação limpa, conhecimento emprestado** | O RSC está reescrevendo o modelo de dados inteiro agora; e o "núcleo caro" é menor do que parecia | | 2 | TypeScript · Hono · SvelteKit · Node | **Go · templ · HTMX · SQLite** | Decisão do projeto; e habilita o binário único | | 3 | Docker Compose de 4 serviços | **Um binário estático, sem dependências** | Diferencial de produto real para auto-hospedagem | | 4 | Item bruto → publicação normalizada (2 camadas) | **Payload → observação → item lógico (3 camadas)** | O mesmo post chega por 3 caminhos diferentes; 2 camadas não resolvem | | 5 | "Percurso" como tela | **Livro-razão de entrega append-only** | Uma estrutura serve Percurso + admin + debug + auditoria | | 6 | Moderação distribuída pelo roteiro | **Moderação e limites no v0.1** | Público real = abuso no dia 1 | | 7 | Vocabulário metafórico completo | **Nomes planos, exceto "Percurso"** | A metáfora era do nome *Afluente*; com *RSS Social* ela perde a âncora | O diagnóstico de **produto** do documento original continua de pé e é a espinha desta proposta. O que mudou foi a premissa técnica e o rigor do modelo de dados. --- ## 1. Diagnóstico: o que a pesquisa no código revelou ### 1.1 O RSC está em cirurgia aberta Último commit em **23/07/2026, 15:17** — o mesmo dia desta proposta. O projeto está no meio de uma reescrita em quatro verticais, atrás da flag `RSC_SOURCE_MODEL_V2=off`, com estratégia declarada de *não fazer dual-write* e **cutover atômico no final**: | Vertical | Substitui | Status em 23/07 | |---|---|---| | V1 — Source Control Plane | Registro de fontes, resolução de URL, governança, auditoria | Implementação iniciada (3 commits) | | V2 — Logical Items & Ordinary Reads | Itens lógicos, convergência, threading, visibilidade, feeds, SSE | Plano aprovado | | V3 — Moderation/Events/Verification | Moderação, tombstones, verificação de origem, evidência | Plano aprovado | | V4 — Migration/Cutover | Conversão atômica dos dados legados | Plano aprovado | São ~3.000 linhas de spec e ~2.900 de plano. **Forkar hoje é congelar o paciente na mesa.** E, ironicamente, o upstream está construindo exatamente o que o documento original listava como diferencial do Afluente: moderação, verificação de origem e convergência determinística. ### 1.2 O "núcleo técnico caro" é pequeno ``` core (backend) ~3.900 LOC (sqlite.ts sozinho: 955) web (SvelteKit) ~4.000 LOC + 939 linhas de CSS total TS/Svelte ~14.400 LOC docs (120 arquivos .md) ~25.000 linhas ← maior que o código ``` Dentro do core, o que é genuinamente difícil de refazer: - `ingest.ts` (268 linhas) — threading resolve-once, orfandade honesta, adoção fora de ordem - `feed.ts` (236) — emissão RSS com namespace `source:`, dual-emit com RFC 4685 - `push.ts` + `push-in.ts` (525) — WebSub e rssCloud, ida e volta - `push-guard.ts` (**58**) — o guard SSRF inteiro - `opml.ts` (146) + `discovery.ts` (91) **~1.500 linhas de lógica de domínio realmente não-trivial.** E o que o documento original chamou de "mais fácil de subestimar" — o parser de RSS/Atom/JSON Feed/OPML — **não está no RSC**. É a biblioteca `feedsmith`. Microformats, identidade, markdown e sanitização também são bibliotecas. Go tem equivalente maduro para cada uma (§4). ### 1.3 O ativo real é conhecimento de interoperabilidade Isto é o que faz uma conversa federar A→B→A por RSS puro. Extraído do código, independe de forkar: ```xml … https://exemplo.org/p/42 texto **em markdown** … ``` As regras que importam: 1. **`guid` = permalink nu** é a chave de thread. É isso que faz o [`threadwalker`](https://github.com/scripting/rss.chat/tree/main/examples/threadwalker) do Dave Winer reconstruir uma conversa **sem alteração nenhuma**. 2. **`source:comments` é um feed RSS por post.** A thread inteira é caminhável um feed por vez, todo nível com a mesma forma. Recursão sobre XML estático, sem API. 3. **`source:markdown` é o marcador de detecção.** É assim que se identifica um par Textcasting: um feed cujos itens carregam esse elemento. 4. **`source:account` é channel-level.** O RSC chegou a emitir por item para agradar uma versão antiga do threadwalker, e depois voltou ao spec. 5. **Edição viaja no `guid` estável** com marcador `` — sem ressuscitar o post no topo da timeline. ### 1.4 O rss.chat é outro animal Também com commit hoje. **MySQL**, não SQLite. **WebSocket**, não SSE. Firehose com framing `verbo\r{json}` e dois verbos (`newItem`, `updatedItem`), campos copiados do FeedLand de propósito — *"gratuitously renaming things is what makes interop hard"*. Auth sem senha: par `emailaddress` + `emailcode`. API por query-string. Cada escrita republica os feeds estáticos do autor, do pai e do feed de comentários do pai. E uma ideia de interface que o documento original não tinha: **hoist / dehoist** — zoom em um nível da conversa, herdado de outliners. Ver §8.3. ### 1.5 Textcasting é um contrato comportamental Não é schema, é manifesto — *"interop entre apps sociais baseado nas features de que escritores precisam"*. As exigências: - títulos **opcionais** - estilo (negrito, itálico) que **sobrevive ao trânsito** - links de qualquer palavra para qualquer URL - **sem limite de tamanho** - **edição que se propaga** depois de publicado - Markdown ao lado do HTML - enclosures (áudio, vídeo, imagem) - **pass-through**: um app que não entende um elemento deve **repassá-lo**, não descartá-lo O último é o mais ignorado do mercado — e o mais barato de honrar (§6.5). --- ## 2. Decisão estratégica: implementação limpa em Go ### 2.1 As três opções, honestamente | | Fork do RSC | Contribuir upstream | **Implementação limpa em Go** | |---|---|---|---| | Aproveita código | ~1.500 linhas úteis, em TS | Todo | Nenhum | | Aproveita conhecimento | Todo | Todo | **Todo** (§1.3) | | Risco de timing | **Alto** — reescrita em curso | Baixo | Nenhum | | Liberdade de stack | Nenhuma (preso a Node) | Nenhuma | **Total** | | Valor de aprendizado | Médio (ler código alheio) | Baixo | **Alto** | | Dívida com terceiros | Merge de upstream para sempre | Roadmap alheio | Nenhuma | | Tempo até algo rodando | Rápido | Rápido | Mais lento | ### 2.2 A escolha e o porquê **Implementação limpa em Go.** Três razões que se somam: 1. **A stack é decisão sua, e ela é incompatível com fork.** Forkar um projeto TypeScript para reescrevê-lo em Go não é fork — é reescrita com passivo jurídico e histórico Git de outra pessoa. Não faz sentido. 2. **O que faria o fork valer a pena não existe lá.** As três camadas que o documento original define como prioridade real — confiabilidade da base, leitura excelente, operação e moderação — reaproveitam quase nada do código do RSC. São justamente o que não está pronto lá. 3. **É um laboratório de aprendizado.** Ler código alheio ensina; construir o modelo de dados difícil ensina muito mais. E os problemas difíceis aqui — convergência determinística, entrega idempotente, SSRF, filas duráveis — são exatamente os que valem aprender. ### 2.3 Postura ética e jurídica Implementação limpa significa: **não copiamos código.** A licença MIT do RSC não se aplica a código que não usamos. Mas a decência se aplica de qualquer jeito: - **Creditar visivelmente**, no README e numa página `/creditos`: - Dave Winer — RSS 2.0, OPML, rssCloud, o manifesto Textcasting, o namespace `source:` e o rss.chat - Ricardo (rmdes) e o RSC — a prova de que a coisa funciona, e o mapa de interop que estudamos - Comunidade IndieWeb — Micropub, Webmention, IndieAuth, microformats2 - JSON Feed — Manton Reece e Brent Simmons - **Não sugerir endosso** de nenhum deles. - **Devolver o que descobrirmos**: bugs de interop, casos de feed do mundo real, divergências de spec. Isso vai para os projetos de origem, não fica aqui. - Licença própria: **MIT** ou **AGPL-3.0** — decisão em aberto (§13). --- ## 3. O produto ### 3.1 Categoria > **Leitor social da web aberta.** Não é "um Mastodon de RSS". Não é um agregador. É um leitor de feeds excelente que, por baixo, também publica e conversa — e que trata a origem de cada conteúdo como informação de primeira classe. ### 3.2 Público Instância **pública** — qualquer pessoa pode se cadastrar (sujeito à política de admissão configurada). Isso muda três coisas em relação a uma instância pessoal: - **Abuso é premissa, não exceção.** Moderação e limites entram no v0.1. - **Perfis públicos são superfície de ataque e de assédio.** Bloqueio, silenciamento e denúncia desde o começo. - **LGPD é real.** Instância brasileira, dados de terceiros, canal de remoção, política de privacidade. Não é opcional. Perfil de quem deve gostar: quem já usa RSS e quer conversa; autores com blog próprio; comunidades IndieWeb e small web; operadores de instâncias pequenas; curadores de diretórios. **Não é para:** milhões de usuários, celebridades, feed algorítmico, descoberta viral. ### 3.3 Proposta de valor, por papel | Papel | Promessa | |---|---| | Quem lê | Tudo que você acompanha, sem algoritmo e sem perder o contexto — e você sempre sabe de onde veio | | Quem publica | Seu domínio é sua identidade; nós distribuímos e reunimos as conversas | | Quem hospeda | Um binário. Um arquivo de banco. Um comando de backup. E um painel que mostra o que quebrou | ### 3.4 O nome **RSS Social** é honesto e posiciona instantaneamente. Custos que aceitamos conscientemente: - Genérico demais para registrar como marca - Difícil de buscar (colide com o termo comum) - Amarra o produto ao nome de um formato Para um laboratório público, o benefício (ninguém precisa te perguntar o que é) supera. **Consequência de vocabulário:** os nomes metafóricos do documento original (*Corrente, Nascentes, Margem, Confluência*) derivavam da metáfora do rio em *Afluente*. Sem essa âncora, viram arbitrários. Recomendação: | Conceito | Nome na interface | |---|---| | Timeline principal | **Feed** | | Site ou feed acompanhado | **Fonte** | | Grupo de fontes | **Coleção** | | Thread de respostas | **Conversa** | | Item salvo | **Salvos** | | Estado de entrega de uma resposta | **Percurso** ← único termo não-óbvio, e merece nome próprio | | Diretório de descoberta | **Descobrir** | --- ## 4. Arquitetura ### 4.1 A tese do binário único Esta é a maior vantagem competitiva que o Go traz, e o documento original não a tinha porque não podia: ``` RSC hoje: Docker + Compose + Caddy + Node(core) + Node(web) + Mailpit RSS Social: ./rss-social ``` Go compila estático. Com um driver SQLite **sem cgo** e `embed` para templates, CSS, JS e migrações, o resultado é **um arquivo**. `scp`, `chmod +x`, rodar. Sem runtime, sem gerenciador de pacotes, sem imagem de container obrigatória, sem processo separado de proxy (TLS via `autocert` embutido ou proxy externo à escolha do operador). Para o público-alvo — pessoas hospedando instâncias pequenas — essa é a diferença entre 5 e 50 instalações. E é o argumento mais forte que temos contra "por que não uso o RSC?". Docker continua existindo como **conveniência opcional**, não como requisito. ### 4.2 Stack | Camada | Escolha | Por quê | |---|---|---| | Linguagem | **Go 1.23+** | Binário estático, concorrência nativa para ingestão, stdlib forte em HTTP/XML/crypto | | HTTP | **`net/http` da stdlib** (roteamento com padrões de método, Go 1.22+) | Sem framework. Menos superfície, menos churn | | Templates | **[`templ`](https://templ.guide)** | Compilado, type-safe, sem overhead de runtime. Erro de UI vira erro de compilação | | Interatividade | **HTMX** + ilhas mínimas de JS | SSR é o modo nativo; "funciona sem JS" sai de graça, não como esforço | | Tempo real | **SSE** (`http.Flusher` da stdlib) | Unidirecional, HTTP puro, reconecta sozinho. ~40 linhas | | Banco | **SQLite**, modo WAL | Um arquivo. Backup é copiar. Sem serviço extra | | Driver SQLite | **[`ncruces/go-sqlite3`](https://github.com/ncruces/go-sqlite3)** | Sem cgo (SQLite compilado para WASM, traduzido para Go puro). FTS5, JSON, vtables. ~3× mais rápido que `modernc` em leitura nos benchmarks públicos. **Custo:** mais memória (sandbox WASM) — validar cedo | | Migrações | **[`goose`](https://github.com/pressly/goose)** com SQL embutido via `embed` | Versionadas, aplicadas no boot, com dry-run | | Parse de feeds | **[`gofeed`](https://github.com/mmcdole/gofeed)** (RSS/Atom/JSON Feed 1.0+1.1) | Guarda namespaces desconhecidos em `Item.Extensions` — exatamente o que `source:` precisa | | Emissão de feeds | **`encoding/xml` à mão** | Controle total do namespace `source:`, da ordem dos elementos e do pass-through. Bibliotecas de geração não dão isso | | Markdown | **[`goldmark`](https://github.com/yuin/goldmark)** | GFM, extensível, é o motor do Hugo | | Sanitização HTML | **[`bluemonday`](https://github.com/microcosm-cc/bluemonday)** | Padrão de fato em Go. É o portão de XSS | | Microformats2 | **[`willnorris.com/go/microformats`](https://pkg.go.dev/willnorris.com/go/microformats)** | h-feed / h-entry / h-card, v1 e v2 | | IndieAuth + Micropub | **[`go.hacdias.com/indielib`](https://go.hacdias.com/indielib)** | Toolkit IndieWeb em Go — **a verificar antes de adotar** | | Webmention | **[`willnorris.com/go/webmention`](https://willnorris.com/go/webmention)** | Do mesmo autor do parser mf2 | | Fila | **Tabela em SQLite, implementação própria** (§5.5) | ~250 linhas. Sem Redis. Sem serviço extra | | Busca | **FTS5 do SQLite** | Já está no driver | | Sessões / senhas | **Próprio**, cookie assinado + `argon2id` (`golang.org/x/crypto`) | Go não tem um `better-auth`; a superfície é pequena o bastante para ser nossa | | E-mail | **SMTP** via `net/smtp` ou `go-mail` | Produção exige SMTP real, sem exceção | | Logs | **`log/slog`** estruturado, stdlib | JSON em produção, texto no dev | | Métricas | **`prometheus/client_golang`** + `/healthz` `/readyz` | Lab de aprendizado: é justamente o que vale aprender | **O que NÃO entra:** Redis, PostgreSQL, MongoDB, ORM, microserviços, Node no build, CDN própria. Cada um entra quando **medição real** demonstrar necessidade — não por antecipação. ### 4.3 Forma do repositório ``` cmd/rss-social/ binário único (serve, migrate, backup, doctor) internal/ feedin/ fetch, parse, descoberta, normalização feedout/ emissão RSS/JSON/OPML, namespace source: convergence/ payload → observação → item lógico (§5.2) thread/ threading resolve-once, órfãos, adoção push/ WebSub + rssCloud, entrada e saída ledger/ livro-razão de entrega (§5.4) jobs/ fila durável em SQLite (§5.5) identity/ contas, sessões, IndieAuth, verificação de domínio moderation/ bloqueios, quarentena, denúncias, purge safety/ guard SSRF, sanitização, limites (§7) store/ SQLite: schema, queries, migrações web/ handlers, templ, HTMX, SSE testdata/ feeds/ corpus de feeds reais, versionado golden/ saída XML esperada, byte a byte docs/ specs/ uma spec por peça decisions/ ADRs ``` Regra: **arquivo com mais de 400 linhas é sinal de que faz coisa demais.** Nenhum acima de 800. --- ## 5. O modelo de dados — onde está o valor real Esta seção é a maior adição desta revisão. O documento original propunha duas camadas (item bruto → publicação normalizada). Isso não resolve o problema central. ### 5.1 O problema que ninguém vê antes de sangrar O mesmo post chega por **três caminhos diferentes**, em momentos diferentes, com conteúdos diferentes: ``` ┌── feed pessoal do autor → tem source:markdown, versão 3 post ───┼── firehose /users/rss.xml → só HTML, versão 2 └── push WebSub da instância dele → tem markdown, versão 3, chegou primeiro ``` Se você guarda "o item", a última escrita ganha e o resultado é aleatório. Um post editado pode voltar à versão antiga porque um poll lento chegou depois de um push rápido. É corrupção silenciosa — o pior tipo. ### 5.2 Três camadas, não duas ``` ┌─────────────────────────────────────────────────────────────┐ │ raw_payload bytes exatos + sha256 + origem + quando │ │ endereçado por conteúdo → dedup de graça │ │ permite reprocessar com parser corrigido │ └──────────────────────────┬──────────────────────────────────┘ │ parse ┌──────────────────────────▼──────────────────────────────────┐ │ observation uma leitura, de uma fonte, num instante │ │ (fonte, payload, campos parseados, updated) │ │ nunca sobrescrita — só acumula │ └──────────────────────────┬──────────────────────────────────┘ │ convergência determinística ┌──────────────────────────▼──────────────────────────────────┐ │ logical_item a identidade, com a versão vencedora │ │ chave: guid canônico (ou link, ou hash) │ │ guarda POR QUE essa versão venceu │ └─────────────────────────────────────────────────────────────┘ ``` **Consequências que valem o custo:** - Corrigir o parser não perde nada — reprocessa `raw_payload` - Deduplicação sai de graça pelo hash - A UI pode mostrar "esta versão veio de X, às Y, por Z" - Auditoria de "o que exatamente aquele site publicou" é possível ### 5.3 Convergência determinística e explicável A regra de seleção, em ordem, **documentada e testada**: 1. Observação de **origem reivindicada pelo domínio do autor** vence origem terceira 2. `atom:updated` (ou `pubDate` na falta) mais recente vence 3. Empate → **maior fidelidade** vence (tem `source:markdown` > só HTML) 4. Empate → **hash lexicograficamente menor** vence O passo 4 é o que importa: é **determinístico, não cronológico**. Duas instâncias que receberam as mesmas observações em ordens diferentes chegam ao **mesmo resultado**. Sem isso, federação diverge. E a interface expõe isso: um item mostra, sob demanda, qual observação venceu e por qual regra. *Aparência robusta é um sistema que explica suas próprias decisões.* ### 5.4 Livro-razão de entrega — uma estrutura, quatro produtos O "Percurso" do documento original vira uma **tabela append-only**: ```sql delivery_attempt( id, logical_item_id, target, protocol, -- websub | rsscloud | webmention attempt_no, http_status, latency_ms, outcome, -- pending|ok|rejected|failed|gaveup error_kind, error_detail, at ) ``` A mesma tabela serve: 1. **A tela Percurso** do autor — "sua resposta chegou?" 2. **O painel do operador** — o que está falhando, e por quê 3. **Debug** — reproduzir uma entrega específica 4. **Auditoria** — registro imutável do que foi tentado Nada é atualizado, só inserido. O estado atual é a última linha. ### 5.5 Fila durável em SQLite ```sql job( id, kind, payload_json, run_after, -- backoff exponencial com jitter lease_until, -- visibility timeout: worker morto libera sozinho attempts, max_attempts, idem_key UNIQUE, -- reexecutar não duplica last_error, created_at ) ``` Workers pegam trabalho com `UPDATE … RETURNING` sob transação. Sem Redis, sem serviço extra, sem perder trabalho quando o processo cai. Dead-letter depois de `max_attempts`, visível no admin com botão de reexecutar. **Idempotência é requisito, não desejo:** reprocessar qualquer job duas vezes não pode duplicar post, resposta ou entrega. ### 5.6 Entidades conta · identidade · site verificado · fonte · assinatura · coleção · `raw_payload` · `observation` · `logical_item` · revisão · conversa · relação de resposta · webmention · `delivery_attempt` · arquivo · regra de moderação · denúncia · `job` · evento de auditoria --- ## 6. Interoperabilidade — o contrato de fio ### 6.1 Entrada RSS 2.0 · Atom · JSON Feed 1.0 e 1.1 · `h-feed`/microformats2 · OPML (com categorias preservadas) · descoberta de feed a partir de página HTML (``) · `ETag`/`Last-Modified` condicional · WebSub e rssCloud como assinante. ### 6.2 Saída Feed RSS e JSON por usuário · firehose da instância em `/users/rss.xml` (convenção do rss.chat) · feed de comentários por conversa · OPML da lista de assinaturas · WebSub fat-ping e rssCloud como publicador. ### 6.3 O namespace `source:` Emitir e consumir, conforme §1.3. Regra dura: **`guid` é o permalink nu.** É o que mantém a compatibilidade com o threadwalker. ### 6.4 Threading `source:inReplyTo` preferido, `thr:in-reply-to` (RFC 4685) como fallback. Resolve-once. **Orfandade honesta**: uma resposta cujo pai não foi resolvido aparece marcada como órfã, não escondida nem inventada. **Adoção**: quando o pai chega depois, o órfão se encaixa. ### 6.5 Pass-through — o requisito que ninguém honra O Textcasting exige que um app repasse elementos que não entende. Ninguém faz. É barato: - Guardar os fragmentos XML desconhecidos junto da observação - Re-emitir na saída, com o namespace preservado Custo: uma coluna e ~60 linhas. Ganho: somos o único leitor da rede que não destrói informação alheia ao trafegá-la. É um argumento de marca, não só técnico. ### 6.6 Suíte de conformidade como portão de CI Não é uma promessa vaga de "testar interop". É concreto: 1. **Corpus versionado** em `testdata/feeds/` — feeds reais, incluindo os quebrados, com seus bugs preservados 2. **Golden files** — a saída XML esperada, byte a byte. Emissão de feed é contrato; se o byte mudou, alguém precisa aprovar 3. **`go test -fuzz`** no parser, no resolvedor de URL e no guard SSRF — fuzzing é nativo em Go e este é o caso de uso perfeito 4. **O threadwalker do Dave roda contra nossos próprios feeds no CI.** Se o walker dele quebrar, o build quebra. Não há teste de interop mais honesto que a ferramenta de referência da outra parte --- ## 7. Segurança ### 7.1 SSRF — do jeito certo em Go Validar a URL **não basta**. `http://evil.com` pode resolver para `127.0.0.1` depois da checagem (DNS rebinding). A técnica correta em Go: ```go // Valida o IP DEPOIS da resolução DNS e ANTES de conectar — em cada hop. dialer := &net.Dialer{ Control: func(network, address string, c syscall.RawConn) error { return denyPrivateAddress(address) // address já é IP:porta resolvido }, } ``` Bloquear: loopback, link-local (`169.254.0.0/16` — inclui metadados de nuvem), redes privadas, multicast, `::1`, IPv4-mapeado em IPv6, `0.0.0.0`. Somado a: só `http`/`https` · limite de redirecionamentos (3) e **revalidação a cada hop** · timeout de conexão e total · limite de bytes (`io.LimitReader`) · validação de `Content-Type` · **nunca** enviar cookies ou credenciais · pool de saída separado do servidor web · registrar destino final e motivo do bloqueio. Isso vale para **todo** fetch remoto: feed, avatar, imagem, verificação de domínio, envio de webmention. ### 7.2 Conteúdo e navegador Sanitização de HTML **no servidor**, sempre, com `bluemonday` — nunca confiar no cliente. CSP restrita com nonce por requisição. Zero script vindo de feeds. `iframe` desabilitado por padrão. Imagens remotas com política configurável (proxy, ou bloquear, ou permitir — decisão do operador, não default silencioso). A mesma cadeia de markdown na prévia e na publicação — o que você vê é o que sai. CSRF em toda ação autenticada. Rate limit em login, publicação, assinatura e callbacks de federação. ### 7.3 Instância pública Cadastro aberto / fechado / por convite / com aprovação — configurável. Quarentena para fonte remota nova. Aprovação de Webmention antes de exibir. Limites por conta, por domínio e por faixa de rede. Bloqueio e silenciamento de pessoa, domínio, fonte, palavra e conversa. Denúncia com contexto. Registro de decisão administrativa. Página pública de regras e contato. ### 7.4 Direitos autorais e LGPD Feed público não é domínio público. Preservar autor, fonte, URL original e data. Exibir apenas o que o feed forneceu. Respeitar `410 Gone`, remoções e edições. Permitir que o operador reduza a exibição a resumo + link. Canal documentado de solicitação de remoção. Política de privacidade e de retenção. Exportação e exclusão de conta a pedido. --- ## 8. Experiência ### 8.1 Princípios 1. **Ler vem antes de postar.** O produto tem que ser bom para quem nunca vai responder. 2. **O domínio é a identidade mais forte.** Três estados, sempre visíveis e nunca confundidos: **conta local** · **site verificado** · **fonte acompanhada**. Um feed importado nunca ganha aparência de verificado só por ter nome e avatar. 3. **Tipos de conteúdo merecem apresentações diferentes.** Nota curta aparece inteira. Artigo mostra título, resumo, tempo de leitura. Podcast tem player e duração. Resposta mostra o trecho ao qual responde. 4. **Estado é conteúdo.** De onde veio, quando chegou, por qual caminho, qual versão, se a fonte está saudável. Visível, não escondido no admin. 5. **Nenhum aprisionamento.** OPML, Markdown, JSON Feed, mídia original, lista de bloqueios, banco inteiro. Excluir conta gera pacote antes de remover. 6. **Funciona sem JavaScript.** Não como cortesia — como arquitetura. SSR é o modo nativo; HTMX enriquece. ### 8.2 Navegação **Hoje** · **Feed** · **Não lidos** · **Conversas** · **Salvos** · **Coleções** · **Descobrir** · **+ Publicar** Desktop em três colunas, onde a terceira é uma **barra de contexto** (fonte selecionada, conversa, percurso da resposta) — **não** um texto "Sobre" estático. Celular em coluna única com cinco destinos na barra inferior. ### 8.3 Hoist / dehoist — emprestado do rss.chat, com crédito Selecionar um post e "elevar" — a timeline some, sobra aquela conversa e suas respostas. Elevar de novo desce mais um nível. Sair volta um nível por vez, com o cursor onde estava. É a melhor ideia de leitura de thread que encontrei na pesquisa e não estava no documento original. Vale roubar, e vale creditar. ### 8.4 Saúde da fonte como superfície do leitor O documento original colocava isso só no admin. Errado — quem lê também precisa: > *Esta fonte não publica há 47 dias.* > *Esta fonte redirecionou permanentemente para outro endereço.* > *As últimas 3 leituras falharam (503).* Com uma ação possível ao lado de cada uma. Feed morto silenciosamente é o principal jeito de um leitor de RSS mentir para o usuário. ### 8.5 Aparência: "editorial instrumentado" O documento original propôs uma direção de revista. Boa, mas incompleta para este produto — porque o diferencial aqui é **transparência de estado**, e revista não tem vocabulário para isso. A direção proposta soma as duas: - **Legibilidade de revista** para o corpo do texto — coluna de 65–75 caracteres, ≥16px, entrelinha 1,55–1,7 - **Instrumentação legível** para estado — cada item carrega uma faixa discreta de procedência (origem · quando chegou · por qual caminho · versão) - **Três papéis tipográficos, não dois:** serifada para leitura · sem serifa para interface · **monoespaçada para estado técnico**. A monoespaçada é o que sinaliza "isto é fato do sistema, não opinião do design" - **Ritmo por divisores e espaço**, não caixas em tudo. Cartão completo reservado para áudio/vídeo, citações, avisos operacionais e coleções - **Densidade é escolha do usuário:** compacto · confortável · editorial - **Claro e escuro, ambos intencionais.** Nem um é inversão do outro - **Cor nunca é a única indicação.** Ícone e texto acompanham sempre - **Movimento explica mudança de estado** e respeita `prefers-reduced-motion`. Item novo não desloca a leitura em curso **Acessibilidade mínima:** WCAG 2.2 AA · navegação completa por teclado · foco visível com dois indicadores além de cor · pular para o conteúdo · região viva silenciosa para itens novos · zoom 200% sem perda de função. *(A paleta concreta fica em aberto — ver §13.)* --- ## 9. Operação ### 9.1 Instalação ```bash curl -LO https://.../rss-social && chmod +x rss-social ./rss-social init # gera config e chaves, cria o banco ./rss-social serve ``` Docker permanece disponível como conveniência, não como requisito. ### 9.2 Comandos do binário `serve` · `init` · `migrate` (com `--dry-run`) · `backup` · `restore` · `doctor` (diagnóstico: banco, migrações, permissões, conectividade, filas travadas) · `version` ### 9.3 Backup e restauração Snapshot consistente do SQLite (`VACUUM INTO`, não cópia de arquivo aberto) + anexos + configuração + chaves + **manifesto com versão de app e de schema** + hash de cada arquivo. **Regra:** backup só pode ser chamado de "funcional" depois de **restaurado automaticamente num ambiente limpo durante o teste de release**. Backup não testado não é backup. ### 9.4 Painel do operador Responde rápido: quais fontes estão quebradas e por quê · quando cada uma atualizou · último status HTTP · quais redirecionaram · quais são duplicatas · quais jobs estão pendentes, travados ou na dead-letter · quais webmentions aguardam moderação · quanto espaço banco/cache/mídia ocupam · quando foi o último backup **testado** · qual versão está instalada e se há migração pendente. **Cada erro traz uma ação possível.** Tentar de novo · editar URL · aceitar redirecionamento · pausar fonte · mesclar duplicata · ver detalhes. ### 9.5 Observabilidade `log/slog` estruturado com ID de requisição · `/healthz` e `/readyz` · `/metrics` Prometheus (duração de ingestão, profundidade da fila, taxa de erro por fonte, latência de entrega) · endpoint de debug protegido. ### 9.6 Releases Versionamento semântico · changelog · migrações testadas nos dois sentidos · teste de restauração no pipeline de release · política de atualização · política de segurança e canal de divulgação de vulnerabilidade. --- ## 10. Roteiro **Estimativas de planejamento, não compromissos.** Uma pessoa com apoio de automação, partindo do zero em Go. ### Etapa 0 — Fundação e prova de interop · 1–2 semanas Antes de qualquer feature: provar que o difícil funciona. - Esqueleto do binário, config, migrações, `doctor` - Guard SSRF **com testes de fuzzing**, incluindo caso de DNS rebinding - Parse de RSS/Atom/JSON Feed contra o corpus real - Emissão de RSS com namespace `source:` e **golden files** - **O threadwalker do Dave caminhando uma conversa nossa, no CI** **Saída:** um binário que não faz nada útil e prova que a parte difícil está certa. ### v0.1 — A espinha · 3–5 semanas O mínimo que é inconfundivelmente *este* produto: post local e item remoto convivendo na mesma timeline, e o laço fechando. - Conta local, sessão, senha e link mágico - Assinar fonte por URL, com descoberta a partir da página - Ingestão com as três camadas (§5.2) e convergência determinística (§5.3) - Timeline SSR única, com SSE - Composição em Markdown, publicação local - Feed RSS e JSON por usuário + firehose da instância - Threading com `source:inReplyTo`, orfandade honesta e adoção - Fila durável e livro-razão de entrega - **Moderação básica: bloqueio, quarentena de fonte nova, rate limit, controle de cadastro** - Backup e restauração **com teste automatizado** **Portão:** duas instâncias federam entre si por RSS puro; e um `restore` num servidor vazio passa no CI. ### v0.2 — Leitura séria · 3–4 semanas Não lidos · salvos · coleções · busca FTS5 · filtros · densidades · tipos de conteúdo com apresentações distintas · saúde da fonte visível · atalhos de teclado · hoist/dehoist · importação e exportação OPML · o sistema visual completo **Portão:** dá para usar diariamente como leitor principal, sem saudade de outro. ### v0.3 — Operação e moderação de verdade · 2–3 semanas Painel completo · denúncias · silenciamento por palavra e domínio · registro de decisões · métricas · releases versionados · migrações com rollback documentado · política pública **Portão:** o operador consegue explicar e recuperar qualquer falha sozinho. ### v0.4 — Domínio como identidade IndieAuth · descoberta de `h-card` e `rel="me"` · reivindicação e desvinculação de domínio · múltiplos sites por conta · estados de verificação explícitos · perfil público com endpoints descobertos ### v0.5 — Conversa entre sites Webmention (envio e recebimento, com moderação) · Micropub · a tela **Percurso** · atualização e exclusão idempotentes · suíte de interop expandida ### v0.6 — Mídia Imagens e galerias · enclosures de áudio e vídeo · player acessível · cotas · remoção configurável de metadados sensíveis · backup incremental ### Depois ActivityPub **como ponte, não como núcleo** · APIs documentadas para clientes alternativos · PWA · diretórios externos --- ## 11. O que não construir App nativo Android ou iOS · mensagens privadas criptografadas · chamadas de áudio ou vídeo · marketplace de plugins · algoritmo de recomendação comportamental · geração ou resumo por IA · microserviços · suporte simultâneo a SQLite, PostgreSQL e MongoDB · CDN própria · monetização · federação ActivityPub completa · promessa de escala massiva. Cada item aumenta manutenção, superfície de ataque e suporte. Nenhum é necessário para provar que um leitor social de feeds pode ser excelente. --- ## 12. Critérios de sucesso ### Experiência Adicionar uma página e descobrir seu feed em menos de três ações · importar OPML preservando pastas · achar não lidos, salvos e conversas sem treinamento · distinguir visualmente artigo, nota, resposta e podcast · operar as funções centrais só com teclado · continuar lendo com JavaScript desligado · não perder a posição quando chegam itens novos · **saber de onde veio cada item sem procurar** ### Correção Duas instâncias com as mesmas observações em ordens diferentes convergem para o **mesmo** item · reexecutar qualquer job não duplica nada · edição remota nunca regride para versão anterior · o threadwalker do Dave caminha nossas conversas sem alteração ### Operação Instalar numa VPS limpa copiando **um arquivo** · atualizar com migração automática e backup anterior · restaurar num servidor vazio no teste de release · identificar fonte quebrada e o motivo pelo painel · exportar tudo sem ferramenta proprietária ### Segurança Bloquear SSRF conhecido, **incluindo via redirecionamento e DNS rebinding** · sanitizar a suíte de payloads XSS · limitar tamanho e tempo de download remoto · moderar webmentions antes de publicar quando configurado · nenhuma porta interna exposta · processo documentado de divulgação de vulnerabilidade ### Desempenho (metas internas, medidas numa VPS definida) Primeira resposta abaixo de **300 ms** no p95 para timeline indexada (o binário único e o SSR permitem ser mais ambicioso que os 500 ms do documento original) · busca abaixo de 300 ms no p95 com 100 mil itens · ingerir 100 feeds heterogêneos sem travar a interface · zero falhas críticas de contraste WCAG 2.2 AA · zero perda de dados no teste automatizado de backup/restore --- ## 13. Questões em aberto — o que preciso decidir com você 1. **Licença.** MIT (máxima adoção, permite fork fechado) ou AGPL-3.0 (obriga quem hospedar modificado a abrir)? Para um projeto sobre não-aprisionamento, AGPL tem um argumento forte. Para um laboratório que quer que outros usem, MIT tem outro. 2. **Fatiamento do v0.1.** Proponho "a espinha" (publicar + ler + federar, tudo raso). A alternativa é "leitor primeiro, publicação no v0.2" — mais fiel ao princípio *ler antes de postar*, mas adia a prova de que o laço fecha. 3. **Paleta e tipografia.** A direção de §8.5 está definida; as cores concretas não. A paleta *Afluente* (teal/argila/musgo) era boa mas amarrada à metáfora do rio. Faço uma proposta nova, adapto aquela, ou você já tem preferência? 4. **Memória do driver SQLite.** `ncruces/go-sqlite3` é rápido e sem cgo, mas o sandbox WASM consome mais memória. **Medir na Etapa 0**, com plano B em `modernc.org/sqlite`. Não é decisão de agora, é medição de agora. 5. **Interop com o rss.chat.** Consumir os feeds dele é trivial e certo. Escutar o firehose WebSocket dele (`wss://rss.chat/`) é opcional — vale a complexidade extra no v0.1, ou fica para depois? 6. **Relação com o RSC.** Vale avisar o Ricardo que existe uma implementação independente em Go que estudou o trabalho dele? Custo zero, ganho provável em troca de casos de interop. Sua chamada. --- ## 14. Riscos conhecidos | Risco | Mitigação | |---|---| | Reescrever do zero é mais lento que forkar | Aceito conscientemente; a Etapa 0 prova o difícil antes de investir no resto | | Interop é frágil e quebra em silêncio | Golden files + threadwalker no CI + corpus versionado | | Convergência determinística é conceitualmente difícil | É o principal valor de aprendizado; especificar e testar antes de codar | | `ncruces/go-sqlite3` é menos maduro que o driver com cgo | Medir na Etapa 0; plano B pronto | | Instância pública atrai abuso antes de haver moderação | Moderação no v0.1, não depois. Cadastro fechado até o v0.3 | | Escopo cresce sozinho | §11 é vinculante. Feature fora dela exige revisão desta proposta | | Um mantenedor só | Binário único e zero dependências de serviço reduzem custo operacional a quase nada | --- ## 15. Fontes **Estudadas em código** (clonadas e lidas em 23/07/2026, depois removidas): [rmdes/rsc](https://github.com/rmdes/rsc) · [scripting/rss.chat](https://github.com/scripting/rss.chat) **Especificações:** [RSS 2.0](https://www.rssboard.org/rss-specification) · [JSON Feed 1.1](https://www.jsonfeed.org/version/1.1/) · [OPML 2.0](https://opml.org/spec2.opml) · [WebSub](https://www.w3.org/TR/websub/) · [Webmention](https://www.w3.org/TR/webmention/) · [Micropub](https://www.w3.org/TR/micropub/) · [IndieAuth](https://indieauth.spec.indieweb.org/) · [microformats2](https://microformats.org/wiki/microformats2) · RFC 4685 (Atom Threading) **Contexto Textcasting:** [textcasting.org](https://textcasting.org/) · [The Future of RSS is Textcasting — kottke.org](https://kottke.org/23/11/the-future-of-rss-is-textcasting-1) · [source.scripting.com](https://source.scripting.com/) **Ecossistema Go:** [ncruces/go-sqlite3](https://github.com/ncruces/go-sqlite3) · [mmcdole/gofeed](https://github.com/mmcdole/gofeed) · [templ](https://templ.guide) · [willnorris.com/go/microformats](https://pkg.go.dev/willnorris.com/go/microformats) · [Go — IndieWeb](https://indieweb.org/Go) · [The Go Frontend Dilemma 2026](https://rajnandan.com/posts/go-frontend-architecture-2026/) · [SQLite driver benchmarks](https://github.com/ncruces/go-sqlite-bench) --- ### Nota de atualidade Escrito em **23 de julho de 2026**. O RSC e o rss.chat receberam commits nesta mesma data e estão em desenvolvimento acelerado — o RSC no meio de uma reescrita de quatro verticais. Confirme o estado de ambos antes de tomar qualquer decisão que dependa deles. As escolhas de biblioteca em Go foram verificadas hoje; `go.hacdias.com/indielib` é a única marcada como **a verificar antes de adotar**.