Multiplataforma de mensagens em tempo real
翻译准备中
取自仓库 README
正在加载完整 README。 以下为已保存的摘录。
Plataforma de mensagens em tempo real — editorial-dark, anti-Discord. Constelações (servidores), órbitas (canais), sussurros (DMs), voz e transmissão de tela, cargos, reações, enquetes, busca, notificações, XP e missões. O desktop é onde o trabalho acontece hoje. O web serviu de mapa: o que ele já resolvia virou o alvo de paridade dos clientes nativos. Saiu do ar e saiu da branch principal em 14/09/2026, e vive em arquivo/web com o histórico inteiro.查看完整 README ↗
Plataforma de mensagens em tempo real — editorial-dark, anti-Discord. Constelações (servidores), órbitas (canais), sussurros (DMs), voz e transmissão de tela, cargos, reações, enquetes, busca, notificações, XP e missões.
Três clientes sobre uma API só:
| Cliente | Onde | Stack | Estado |
|---|---|---|---|
| Desktop | mobile-native/desktopApp | Kotlin · Compose Multiplatform/JVM | fase ativa |
| Android | branch arquivo/android | Kotlin · Jetpack Compose | arquivado |
| Web | branch arquivo/web | React 19 · Vite | arquivado |
O desktop é onde o trabalho acontece hoje. O web serviu de mapa: o que ele já
resolvia virou o alvo de paridade dos clientes nativos. Saiu do ar e saiu da branch
principal em 14/09/2026, e vive em arquivo/web com o histórico inteiro.
O Android saiu da branch principal em 06/09/2026 e vive em arquivo/android, com
o histórico inteiro. Nenhuma release traz APK, e o APK/README.md diz isso a quem
vai procurar: a última compilação que chegou a existir é muito anterior à versão do
desktop, e entregá-la seria passar um Astra atrasado por atual. A release do desktop
publica só o que o app usa: o pacote e o hash dele.
O desktop tem ainda uma quarta peça, que não é um cliente: sidecar-voz, um
processo em Go que cuida da voz e da transmissão de tela. Ele é lançado pelo
aplicativo e morre com ele.
O código não tem comentários, e isso é deliberado. O porquê de cada decisão — as medições, as armadilhas, os caminhos tentados que não deram certo — está no histórico de commits, que é onde ele fica pesquisável por
git log -Se amarrado à mudança que o produziu.
Backend (apps/api) — 39 grupos de rota
sidecar-voz)IA_PROVIDER desempata)Desktop (mobile-native/desktopApp) — a fase ativa
Voz e tela do desktop (sidecar-voz) — um processo à parte, em Go
Android (arquivado na branch arquivo/android)
Web (branch arquivo/web, arquivado)
Monorepo: npm workspaces · packages/types (Zod compartilhado). O projeto
nativo é um build Gradle à parte em mobile-native/, e sidecar-voz/ é um módulo
Go independente — o Gradle do desktop o compila e empacota junto do aplicativo.
:sharednão é KMP. Apesar do nome, ele tem só o source setmain, é Kotlin/JVM puro e carrega apenas a camada de rede (core/network) e os DTOs. A UI não é compartilhada entre Android e desktop — cada um tem as próprias telas. Paridade entre eles é reescrita, não reuso; planejar como se fosse reuso já custou tempo aqui.
Hospedagem: API → Render (US East) · Postgres → Supabase · Redis → Upstash · arquivos → bucket S3 ou R2 · voz e tela dos três clientes → LiveKit Cloud. O web saiu da lista quando foi arquivado.
Conversa
Órbitas de texto e de voz, categorias, sussurros (DM), grupos, respostas, edição,
menções com @ e autocomplete, reações, enquetes, GIFs, figurinhas por
constelação, emojis, anexos com prévia e lightbox, marcadores, busca global,
histórico de destinos, tradução, fixar mensagem e menus de botão-direito em tudo.
A mensagem guarda quem a escreveu no instante do envio — nome, foto e cor —, então
trocar de perfil não reescreve o passado. Mensagem anterior a essa regra cai no perfil
atual: não há de onde tirar a identidade da época, e carimbá-la seria inventar.
Voz e tela Sala de voz por órbita com antessala, chamada de voz dentro do sussurro com registro no histórico ("Chamada perdida", "Chamada de 12 min"), transmissão de tela, painel flutuante da call ao navegar, soundboard por constelação. Não há câmera: o Astra é voz e tela, por decisão de escopo. Volume de entrada e de saída, volume por pessoa no botão-direito do cartão, e um sinal de três barras no rodapé que mostra ida e volta, tremor e perda da chamada.
Pessoas Amigos com pedidos nos dois sentidos, presença ao vivo (online/ausente/ocupado/ invisível), bloqueio, perfil com avatar, banner, pronomes, bio, recado, cor e fonte próprias, recorte de imagem embutido, cartão de perfil completo, cargos com cores/hierarquia/permissões, banimentos, convites e prévia de convite.
A bot da casa
Duas personas que se revezam: Sparkle na semana, Sparxie no fim de semana.
A troca de turno é anunciada no canal, e a que estava de plantão fica gravada na
mensagem — quando a outra assume, o aviso antigo continua assinado por quem o
escreveu. Ela saúda quem chega na constelação, comemora nível, e atende por
/sparkle ou /sparxie com 21 comandos entre utilidade e entretenimento (ajuda,
status, sorteio, dado, enquete-relâmpago, "quem mandou"). Conversa por IA: Groq,
com Gemini como alternativa.
Descoberta e chegada Descobrir constelações públicas, entrada por convite, onboarding com checklist de primeiros passos, constelação nova nasce povoada.
Sinal Notificações com painel, badge, marcar tudo como lido e limpar histórico, silenciar por canal e por constelação, não-lidos com contagem, push (Android). Silenciar tudo por uma hora ou até de manhã vale só nesta máquina, por escolha — quem quer calar a conta inteira já tem o "não perturbe" — e o rodapé mostra um sino cortado enquanto o prazo corre, que desfaz num clique.
Progressão XP por mensagem e por tempo em call, níveis com anel em volta da foto, missões com aviso, distintivos.
Casa da máquina (desktop)
Auto-update por zip-swap assinado com Ed25519, que baixa só o que mudou, insiste
quando a conexão oscila, retoma de onde parou o download do pacote inteiro e só mostra
a versão nova ao atalho quando ela está inteira, bandeja do sistema que troca o
estado (brilhando, ausente, não perturbe, invisível) e silencia os avisos sem abrir
a janela, atalho no menu iniciar, paleta de comandos (Ctrl+K), diagnóstico de rede
e permissões,
configurações com prévia ao vivo por aba, aparência (aurora, estrelas, ou as
duas), 19 cores de acento, três níveis de gráficos, modo de reduzir movimento.
Abrir o Astra começa pela tela de abertura, que procura versão nova e conta o que está acontecendo — as palavras espaciais enquanto procura, o nome da versão quando há uma, o progresso quando baixa. Ela sai sozinha: na hora, se estiver em dia; depois de dizer o motivo, se a consulta falhar. Se há versão nova, ela fica até a atualização terminar e o Astra reabrir já atualizado — a não ser que a conexão passe um minuto inteiro sem entregar nada; aí ela dá lugar à janela, e a atualização segue por trás, com um aviso na barra de título. E ao sair entrega o lugar para a janela principal.
O Astra não mede a máquina de quem usa. Não há afinação automática, degrau escondido nem qualidade escolhida por número de núcleos: todo mundo recebe o mesmo app. Quem cria conta escolhe um perfil — Leve, Equilibrado ou Completo — num passo do tour de estreia, com a tela mudando ao vivo enquanto escolhe, e tudo continua item a item em Aparência.
Quando algo dá errado, o app deixa laudo em %LOCALAPPDATA%\Astra:
arranque.txt marca por onde o arranque passou e em quanto tempo — onde a lista
para é onde o app parou; diagnostico.txt traz placa, motor de desenho e versão;
falhas.txt guarda o que estourou; voz.txt recebe o relato do sidecar;
recuo.txt diz que uma segunda cópia reconheceu a primeira e saiu — e, quando foi
o caso, que ela encerrou uma primeira que havia travado.
Só um Astra por vez, e sem pedir nada à rede. Quem responde "já há um aberto?" é
um cadeado nomeado do Windows: o sistema o solta no instante em que o processo
termina, por saída limpa, queda ou encerramento forçado — então fechar e reabrir na
hora seguinte simplesmente abre. O recado "traga a janela para a frente" viaja por um
socket de arquivo (aviso.sock), não por porta de rede, para que o firewall não tenha
nada a decidir. Onde o caminho do arquivo passa do teto do Windows (~108 caracteres),
o Astra recua sozinho para uma porta em 127.0.0.1.
Não é item de backlog aqui — é regra de aceite para tela nova:
:focus-visible da web) — clicar com o mouse não desenha anel.jdk.accessibility precisa continuar no empacotamento.Requer Node 24, PostgreSQL e Redis. Para o desktop, JDK 21.
# 1. Instalar deps
npm install
# 2. Configurar envs (copia .example, preenche)
cp apps/api/.env.example apps/api/.env
# 3. Migrar DB
npm run db:migrate
# 4. Dev (API)
npm run dev
API em http://localhost:3001. O desktop aponta para ela sozinho em desenvolvimento.
Desktop:
cd mobile-native
./gradlew :desktopApp:run # roda
./gradlew :desktopApp:compileKotlin # só compila (checagem rápida)
./gradlew :desktopApp:zipDistributable # zip distribuível
Android (arquivado — só na branch arquivo/android):
git switch arquivo/android
cd mobile-native
./gradlew :app:assembleDebug
Postgres e Redis são serviços externos, não add-ons do Render.
DATABASE_URL (a conexão direta é IPv6 e o Render não alcança; a 6543 é o
transaction pooler e quebra os prepared statements do Drizzle)rediss://) → REDIS_URLnpm run build:apinpm run start:apiapps/api/.env.example):
DATABASE_URL (Supabase) · REDIS_URL (Upstash)JWT_ACCESS_SECRET + JWT_REFRESH_SECRET
(gere com node -e "console.log(require('crypto').randomBytes(48).toString('hex'))")GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRETCLIENT_URL = URL pública do cliente web, sem barra final. Obrigatória: a API
usa em CORS, no CSP, no retorno do login Google e no destino dos convites.
O site foi arquivado, então hoje ela aponta para um endereço fora do arAPI_URL = URL pública desta API (sem barra final)Sem passo de migration: o schema é garantido no boot por ensureSchema
(DDL idempotente).
O plano free dorme após ~15min sem tráfego de entrada — a API se cutuca sozinha no
/live a cada 10min (lib/naoDormir.ts, só em produção e só com RENDER_EXTERNAL_URL
ou API_URL), e um pinger externo (ex: cron-job.org) cobre o resto, também no /live.
Aponte o pinger para o
/live, não para o/health. O/healthconsulta Postgres e Redis a cada chamada: pingado de minuto em minuto ele segura o banco acordado sem necessidade — foi assim que a cota de compute do Neon estourou em 2026-07-31 (erro53000: exceeded the compute time quota) e derrubou o deploy, antes da mudança para o Supabase./livesó responde uptime.
Acordado 24h o serviço consome ~744 das 750 horas free por mês da conta inteira. Cabe, com folga curta: um segundo serviço free na mesma conta suspende todos até o mês virar. O Render também reinicia serviços free quando quer, e voltar leva ~1min — no plano free não existe "nunca cair".
Publicar é só subir a versão. Não há tag para criar nem zip para arrastar:
mobile-native/gradle.properties → astraVersion=x.y.zastra-dev para a main, juntado com commit de merge quando a CI
passar — a main não aceita push diretoO workflow desktop-release.yml trabalha em duas etapas separadas. A primeira monta o
zip e o prova, com permissão só de leitura. A segunda não baixa nem executa nada do
build: gera a lista com o SHA-256 de cada arquivo do pacote, assina essa lista com
Ed25519, confere a assinatura contra a chave pública embutida no app e só então cria a
tag desktop-v<versão> com o zip, o hash, a lista e a assinatura. O app procura
sozinho e só instala se a assinatura bater com a chave dele e cada arquivo montado
bater com a lista, nos dois caminhos de atualização.
A chave privada vive só no segredo ASTRA_CHAVE_DE_ASSINATURA do repositório, e
não existe reserva. Sem ela nenhuma release sai; se ela se perder ou vazar, quem estiver
numa versão que confere assinatura só volta a atualizar reinstalando à mão. As
ferramentas estão em tools/assinatura.
Antes de publicar, o zip recém-montado é aberto treze vezes, em treze condições
diferentes (tools/provas-de-abertura): máquina fraca, sem placa de vídeo, modo
seguro, preferências corrompidas ou travadas, sessão pela metade, sem internet,
abertura depois de uma queda, uma segunda cópia com o app já aberto, a vaga da cópia
única presa por um programa que nunca responde, e fechar e reabrir no instante
seguinte. Se qualquer uma falhar, a publicação para — nada chega a ninguém.
Desenhar um quadro é necessário e não é suficiente: uma janela pode desenhar atrás de todas as outras, e para quem está na frente do computador isso é igual a não abrir. Por isso as provas de segunda cópia e de reabertura cobram também que a janela que já existia seja trazida para a frente, e que a vaga fique livre depressa.
As provas são Python de biblioteca padrão e rodam à mão:
python -u tools/provas-de-abertura/provas.py --de <pasta do app> --tempo 75
O banco recusa começar se já houver um Astra aberto — uma cópia viva faria toda prova sair pelo caminho errado. E use
--basecom uma pasta curta: o recado entre cópias viaja por arquivo, e caminho de arquivo tem teto de ~108 caracteres no Windows; acima disso as provas exercitam o recuo em vez do caminho principal.
O app minimiza para a bandeja em vez de fechar. Para testar um build novo, use Sair na bandeja e reabra — fechar a janela não encerra o processo.
https://<client-url>/auth/callback em
"Authorized redirect URIs"Convites apontam para o site arquivado.
GET /invite/:codemonta o cartão de prévia do link e depois manda a pessoa paraCLIENT_URL/invite/<code>. Com o site fora do ar, quem recebe um convite cai numa página morta. Pendente decidir o novo destino.
npm run dev # api (com predev hook)
npm run dev:fast # mesmo, sem predev (skip migrate + port check)
npm run build # build types + api
npm run build:api # mesmo que build
npm run db:migrate # migrations Drizzle
# API workspace
npm test -w apps/api # vitest
npm run db:studio -w apps/api # Drizzle Studio
A API sobe com qualquer uma destas vazia — a funcionalidade fica desligada em fallback, e não quebra o boot:
LIVEKIT_* — sem isso, voz e tela off nos três clientesGROQ_API_KEY / GEMINI_API_KEY — sem nenhuma das duas, a bot fica offS3_* / R2_* — em desenvolvimento, o upload cai para o disco local
(storageMode = local). Em produção ele é RECUSADO com 503: o disco do
servidor é efêmero, e gravar ali produziria URLs que morrem no próximo deploy,
deixando a imagem quebrada no banco para sempre. O log diz quais variáveis faltamBREVO_API_KEY + MAIL_FROM — sem isso, e-mail offGIPHY_API_KEY — sem isso, o seletor de GIF someVAPID_* — sem isso, push offSENTRY_DSN — sem isso, sem error trackingMETRICS_TOKEN — sem isso, /metrics responde 404 em produçãoEditorial-dark "obsidiana", dark-only por escolha. O acento de fábrica é
branco (#D4D8E0, preset "Obsidiana") — âmbar é uma opção entre 19, não o
padrão. O fundo padrão é liso; aurora e estrelas são escolha em
Aparência › Fundo, e dá para ligar as duas.
Três regras carregam o resto:
void → base → raised → overlay → hover → active é a ferramenta principal de "isto importa mais". Cor
entra por último e em pouca área — o acento nunca vira fundo.Tipografia: Cinzel (letreiro) + Cormorant (títulos) + DM Sans + DM Mono + Great Vibes.
Paleta completa de tokens do desktop em mobile-native/desktopApp/…/ui/theme/.
Nem tudo que dá cara ao Astra foi feito aqui. O que veio de fora está nomeado abaixo, com o autor e a origem.
Regra da casa: todo asset de terceiro entra junto do texto da licença dele e de uma linha nesta seção, no mesmo commit. Várias dessas licenças pedem atribuição, e mesmo as que não pedem merecem — a arte é de quem a desenhou.
| O quê | Autor | Origem |
|---|---|---|
| Gato do companheiro — "Simples" | Elthen (ELV Games) | 2D Pixel Art Cat Sprites |
| Gato do companheiro — "Travesso" | Jump Button (@Jump_Button) | Cat Player — crédito exigido em uso comercial |
| Companheiro — "Sátiro" | autor a confirmar | pacote sem arquivo de licença; uso liberado pelo autor ao dono do Astra |
A licença dos companheiros viaja junto da arte, em
mobile-native/desktopApp/src/main/resources/pet/. A geometria de recorte de cada
folha está documentada no enum Bicho, em ui/GatoDoAstra.kt.
Feitas por animadores e publicadas no LottieFiles sob a
Lottie Simple License, que permite uso comercial e não exige crédito — o crédito aqui
é a regra da casa. Os arquivos ficam sem alteração em
mobile-native/desktopApp/src/main/resources/animacoes/, com o texto da licença ao
lado; a cor do tema é aplicada na hora de tocar, pela biblioteca
Compottie (MIT).
| O quê | Autor | Origem |
|---|---|---|
| Brilho da estrela de enviar | Cristina Loreto V H (variação de "stars points") | star shine |
| Constelação que se forma (coluna e Descobrir) | Abdul Latif | Constellation icon animation |
| Eclipse sobre a sua foto ao escolher "não perturbe" | Anh Nguyen Duc | eclipse loading |
| Lua de sangue sobre a sua foto ao escolher "ausente" | can kankaya, edição de "phase of the moon", de Moyu | moon-phases-edited |
| Fonte | Autor | Licença |
|---|---|---|
| Cormorant | Christian Thalmann (Catharsis Fonts) | SIL Open Font License 1.1 |
| DM Sans · DM Mono | Colophon Foundry / Jonny Pinhorn | SIL Open Font License 1.1 |
| Cinzel (letreiro "Astra") | Natanael Gama (NDISCOVER) | SIL Open Font License 1.1 |
| Great Vibes | Robert Leuschke | SIL Open Font License 1.1 |
O texto de cada licença viaja junto do arquivo da fonte, em
mobile-native/desktopApp/src/main/resources/font/LICENCA-*.txt. Não é
formalidade: a SIL OFL exige que a licença acompanhe o Font Software sempre que
ele for redistribuído, e o Astra é um zip público — a linha nesta tabela credita, mas
quem cumpre a licença é o arquivo ao lado da fonte.
Lucide — ISC License.
A lua do estado "ausente" é o ícone "eclipse" (sólido) da
Myna UI Icons, de Praveen Juge — MIT
License. O arquivo fica sem alteração em
mobile-native/desktopApp/src/main/resources/icones/, com o texto da licença ao lado.
| Biblioteca | Para quê | Licença |
|---|---|---|
webrtc-audio-processing d0569cfa | o cancelador de eco AEC3, o mesmo que roda dentro do Chromium | BSD 3-Clause (Google / projeto WebRTC) |
Esta entra compilada dentro do astra-voz.exe, então a licença viaja junto do
código que a usa, em sidecar-voz/eco3/. Ela existe por um motivo específico: o
cancelador de eco do Windows só opera em 8 ou 16 kHz, e ligá-lo derrubava toda a
voz do Astra para 16 kHz — metade da taxa que o site usa. O AEC3 cancela na taxa
cheia de 48 kHz.
O ponto travado é um commit, não a versão 2.1 lançada: a 2.1 não compila com
GCC 15 ou 16, porque essas versões deixaram de arrastar <cstdint> junto de outros
cabeçalhos e o código conta com isso. O d0569cfa é o primeiro estado já corrigido.
O catálogo que traduz nome de executável em título de jogo
(mobile-native/desktopApp/src/main/resources/jogos.tsv) é destilado da lista de
aplicações detectáveis que o Discord publica em
discord.com/api/v9/applications/detectable.
Ser honesto sobre o que isso é: um endpoint público mas não documentado, sem licença declarada. Os títulos são marcas dos respectivos donos e aparecem só para dizer o que a pessoa está jogando.
Um segundo arquivo (jogos-arte.tsv) guarda, por título, o identificador do
aplicativo e a impressão do ícone. Com esses dois o Astra monta o endereço da arte
do jogo. Ela não é servida do Discord: o servidor do Astra busca a imagem uma
vez, converte, guarda no armazenamento próprio e serve dali em diante. Isso é
deliberado, por duas razões — nenhuma máquina de usuário fala com o Discord, e o que
já foi guardado sobrevive se a fonte sair do ar.
Sobre a arte em si: os ícones são das desenvolvedoras dos jogos, não do Discord, que apenas os hospeda. O Astra os reexibe para identificar o jogo que alguém está jogando — o mesmo uso nominativo dos títulos. Nenhum ícone é redistribuído no instalador; eles só existem no armazenamento do servidor, e só os dos jogos que alguém aqui de fato abriu.
O instantâneo tem data no cabeçalho de cada arquivo e se refaz com
node tools/atualizar-catalogo-de-jogos.mjs. Se a fonte sair do ar, o Astra continua
funcionando: o catálogo é a primeira tentativa, a descrição do próprio executável
segue como resposta, e o desenho vetorial cobre quem não tem arte.
| Biblioteca | Para quê | Licença |
|---|---|---|
Opus (opus-0.dll) | codec de voz das chamadas | BSD 3-Clause |
| Pion WebRTC | transporte de mídia das chamadas | MIT |
| LiveKit server-sdk-go | entrar na sala e publicar voz e tela | Apache 2.0 |
| MP3SPI + JLayer | leitura de MP3 nos sons da soundboard | LGPL 2.1+ |
| VorbisSPI + JOrbis | leitura de OGG nos sons da soundboard | LGPL 2.1+ |
Tritonus (tritonus-share) | base comum dos dois provedores acima | LGPL 2.1+ |
Nucleus (nucleus.notification-windows + nucleus.core-runtime) | gravar a identidade do Astra no atalho do menu Iniciar | MIT |
| Ferramenta | Para quê | Licença |
|---|---|---|
| goversioninfo | gerar o recurso que dá nome e ícone ao astra-voz.exe no Windows | MIT |
| MinGW-w64 via winlibs (GCC 16.1.0) | compilar o AEC3, que é C++, para dentro do astra-voz.exe | GPL 3 com a Runtime Library Exception |
O goversioninfo não entra no produto: ele roda uma vez, à mão, e o que fica
versionado é a saída (sidecar-voz/resource_windows_amd64.syso, gerada de
versioninfo.json). Foi escolhido assim de propósito — gerar a cada build
acrescentaria uma dependência de rede ao empacotamento e faria o binário do sidecar
mudar em toda release, o que custaria 9,58 MB de download por atualização a quem já
o tem.
O MinGW é caso diferente, e por isso está creditado: a ligação é estática
(-static-libstdc++ -static-libgcc), então pedaços da biblioteca de execução do GCC
viajam dentro do executável distribuído. A Runtime Library Exception existe
exatamente para isso e não impõe a GPL ao resultado — o crédito aqui é a regra da
casa, não exigência da licença.