---
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/42texto **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**.