RSS Social — leitor social da web aberta
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.
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 e do 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 ordemfeed.ts(236) — emissão RSS com namespacesource:, dual-emit com RFC 4685push.ts+push-in.ts(525) — WebSub e rssCloud, ida e voltapush-guard.ts(58) — o guard SSRF inteiroopml.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:
<rss version="2.0" xmlns:source="http://source.scripting.com/">
<channel>
<source:account>…</source:account> <!-- channel-level, NÃO item-level -->
<item>
<guid isPermaLink="true">https://exemplo.org/p/42</guid>
<source:markdown>texto **em markdown**</source:markdown>
<source:inReplyTo isPermaLink="false">…</source:inReplyTo>
<thr:in-reply-to ref="…"/> <!-- RFC 4685, fallback -->
<source:comments count="7" feedUrl="https://exemplo.org/p/42/replies.xml"/>
</item>
</channel>
</rss>
As regras que importam:
guid= permalink nu é a chave de thread. É isso que faz othreadwalkerdo Dave Winer reconstruir uma conversa sem alteração nenhuma.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.source:markdowné o marcador de detecção. É assim que se identifica um par Textcasting: um feed cujos itens carregam esse elemento.source:accounté channel-level. O RSC chegou a emitir por item para agradar uma versão antiga do threadwalker, e depois voltou ao spec.- Edição viaja no
guidestável com marcador<atom:updated>— 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:
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.
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á.
É 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
- Dave Winer — RSS 2.0, OPML, rssCloud, o manifesto Textcasting, o namespace
- 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 |
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 |
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 com SQL embutido via embed |
Versionadas, aplicadas no boot, com dry-run |
| Parse de feeds | 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 |
GFM, extensível, é o motor do Hugo |
| Sanitização HTML | bluemonday |
Padrão de fato em Go. É o portão de XSS |
| Microformats2 | willnorris.com/go/microformats |
h-feed / h-entry / h-card, v1 e v2 |
| IndieAuth + Micropub | go.hacdias.com/indielib |
Toolkit IndieWeb em Go — a verificar antes de adotar |
| Webmention | 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 |
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:
- Observação de origem reivindicada pelo domínio do autor vence origem terceira
atom:updated(oupubDatena falta) mais recente vence- Empate → maior fidelidade vence (tem
source:markdown> só HTML) - 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:
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:
- A tela Percurso do autor — "sua resposta chegou?"
- O painel do operador — o que está falhando, e por quê
- Debug — reproduzir uma entrega específica
- 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
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
(<link rel=alternate>) · 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:
- Corpus versionado em
testdata/feeds/— feeds reais, incluindo os quebrados, com seus bugs preservados - Golden files — a saída XML esperada, byte a byte. Emissão de feed é contrato; se o byte mudou, alguém precisa aprovar
go test -fuzzno parser, no resolvedor de URL e no guard SSRF — fuzzing é nativo em Go e este é o caso de uso perfeito- 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:
// 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
- Ler vem antes de postar. O produto tem que ser bom para quem nunca vai responder.
- 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.
- 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.
- 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.
- Nenhum aprisionamento. OPML, Markdown, JSON Feed, mídia original, lista de bloqueios, banco inteiro. Excluir conta gera pacote antes de remover.
- 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
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ê
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.
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.
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?
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 emmodernc.org/sqlite. Não é decisão de agora, é medição de agora.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?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 · scripting/rss.chat
Especificações: RSS 2.0 · JSON Feed 1.1 · OPML 2.0 · WebSub · Webmention · Micropub · IndieAuth · microformats2 · RFC 4685 (Atom Threading)
Contexto Textcasting: textcasting.org · The Future of RSS is Textcasting — kottke.org · source.scripting.com
Ecossistema Go: ncruces/go-sqlite3 · mmcdole/gofeed · templ · willnorris.com/go/microformats · Go — IndieWeb · The Go Frontend Dilemma 2026 · SQLite driver benchmarks
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.