Projeto independente
Uma biblioteca para todas as fontes de manga, em iOS e Android
Um leitor de manga self-hosted com aplicações nativas para iOS e Android sobre um back-end em Rust, que unifica muitas fontes numa só biblioteca privada que se lembra exatamente de onde ficou.
- Resultado
- Uma biblioteca sobre muitas fontes, lida nativamente em ambas as plataformas, offline ou online, com o progresso a acompanhar o leitor
- A decisão pouco convencional
- Duas aplicações nativas mantidas honestas por queries GraphQL idênticas byte a byte sobre um só back-end em Rust, para que iOS e Android nunca possam divergir.
- Disciplinas
- Engenharia de SoftwareInfraestruturaDesign
- Tags
- Aplicação móveliOS & AndroidRustGraphQL
- Entregue
- julho de 2026
Mantive um velho telemóvel Android vivo só para ler manga, porque os leitores de iOS não eram suficientemente bons. É uma razão tola para andar com um segundo telemóvel, por isso construí o leitor que eu queria.
Atlantic Forge
O que estava em jogo
Durante anos mantive um velho telemóvel Android vivo para exatamente uma tarefa: ler manga. Aos leitores de iOS faltam demasiados dos pequenos confortos que contam ao longo de uma série comprida, e por isso o segundo telemóvel ficava. E mesmo no Android, as boas opções não eram boas: personalização que pára uma definição antes daquilo que realmente queremos, e anúncios a interromper a única atividade que devia ser um prazer. Entretanto, o manga em si vive espalhado por dezenas de fontes, cada uma com o seu site atulhado, o seu próprio catálogo, e nenhuma memória de onde ficámos.
A esta altura da página já se nota um padrão: a ferramenta que eu queria não existia, por isso construí-a. Mas “um bom leitor” é uma promessa mais difícil do que parece. Significa um back-end a apresentar um catálogo calmo e estável sobre um mundo confuso e instável de fontes, e dois clientes nativos que têm de concordar com ele e um com o outro, perfeitamente, para sempre. Três bases de código a descrever uma biblioteca são três oportunidades de divergir, e um leitor nota a divergência de imediato: um marcador que existe no telemóvel mas não no tablet, um capítulo marcado como lido que volta por ler.
A decisão
A concordância é imposta, não desejada. Os dois clientes nativos partilham queries GraphQL idênticas byte a byte contra um só schema, pelo que, se o contrato muda, ambas as aplicações o sentem em tempo de compilação, em vez de ser o leitor a senti-lo à noite. E o back-end guarda todos os detalhes feios nos bastidores: agregação, failover, cache de imagens. Uma fonte pode abrandar, mudar ou desaparecer, e nenhuma das aplicações muda uma linha.
As aplicações pedem uma série, nunca uma fonte. A confusão fica nos bastidores, onde pertence.
A outra escolha deliberada foi para quem é. O Paneru é self-hosted, com contas para um pequeno círculo de leitores convidados, não um serviço público. Isso mantém o sistema honesto quanto ao seu propósito: uma biblioteca privada, a correr em hardware que o dono controla, ao serviço das pessoas para quem foi construída.
O que entregámos
Um sistema self-hosted em três partes. Um back-end em Rust (Actix, async-graphql, Postgres) agrega o MangaDex e várias outras fontes num catálogo único atrás de uma só API GraphQL, com proxy de imagens, cache em WebP e contas com JWT. Por cima, dois clientes nativos: iOS em SwiftUI (iOS 17+) e Android em Kotlin com Jetpack Compose. Ambos oferecem uma biblioteca unificada, leitura paginada e em webtoon, downloads offline que se sentem idênticos a ler online, sincronização de progresso entre dispositivos, marcadores, um atalho de continuar a ler e notificações de capítulos novos. O design segue uma única regra: as capas são a interface enquanto se navega, e o leitor em si é silencioso, com um modo preto AMOLED, um ajuste de brilho próprio e fundos quentes para longas sessões noturnas. Por outras palavras, os pequenos confortos pelos quais o segundo telemóvel se mantinha vivo, finalmente num só lugar, sem anúncios em lado nenhum.
A stack, e porquê
Rust, em vez de Node ou Python. Todo o trabalho do back-end é concorrente e sem glamour: fazer scraping de várias fontes ao mesmo tempo, servir e guardar em cache milhares de imagens, e manter-se de pé sem vigilância numa pequena máquina self-hosted. Um serviço em Node ou Python teria sido mais rápido de escrever, e para um primeiro protótipo essa troca é muitas vezes a certa. Aqui o serviço corre sem ninguém a olhar, em hardware modesto, onde as garantias em tempo de compilação do Rust e a sua pegada pequena valem o arranque mais lento: nenhum runtime para afinar, nenhuma memória a crescer para perseguir, e uma classe de bugs de concorrência que não pode ser lançada porque não pode compilar.
SwiftUI e Jetpack Compose, em vez de Flutter ou React Native. Um leitor vive ou morre pela sensação: a física de scroll de uma longa tira webtoon, carregamento de imagens que nunca engasga, e integração profunda com a plataforma para downloads e notificações. As frameworks multiplataforma são honestas quanto à sua troca: uma só base de código, ao custo de viver a um passo da plataforma. Para uma aplicação de conteúdo essa troca pode ser certa; para uma superfície de leitura é o próprio produto a ser comprometido. Duas aplicações nativas custam mais a construir, e o contrato GraphQL partilhado recupera a maior parte desse custo ao manter a lógica de ambas idêntica.
Postgres, em vez de um armazém de documentos. Uma biblioteca de manga parece documentos até a descrevermos: as séries têm capítulos, os capítulos existem em várias fontes, os leitores têm progresso por capítulo e marcadores por série. Isso são relações e joins, e o Postgres responde-lhes diretamente. Um armazém de documentos empurraria esses joins para o código da aplicação, que é exatamente onde um sistema de três bases de código não precisa de mais lógica para manter em concordância.
O que mudou
- Uma biblioteca privada substituiu um ritual semanal de separadores, pesquisas e lugares perdidos
- Ler sabe ao mesmo offline e online, e o progresso acompanha o leitor entre dispositivos
- As fontes podem mudar e falhar atrás da API sem nenhuma das aplicações dar por isso
- Ambas as plataformas recebem um leitor genuinamente nativo, não um site embrulhado
