← back to the garden

MD

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.

  • pesquisa
  • rss-social
  • rss
  • textcasting
  • go
  • web-aberta
  • self-hosting

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 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:

<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:

  1. guid = permalink nu é a chave de thread. É isso que faz o 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 <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:

  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 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
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:

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

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:

  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:

// 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

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 · 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.