Serein
Rust로 만든 가볍고 성능 좋은 Discord 전용 네이티브 클라이언트
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기Voz, vídeo, transmissão de tela e chat para grupos de amigos. Sem anúncios e sem assinatura.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Um app estilo Discord para a galera: comunidades com canais de texto, salas de voz, câmera e compartilhamento de tela. Cada comunidade é um "servidor" no sentido do Discord: tem os próprios canais, membros, emojis, sons e cargos, e entra-se nela por um código de convite. Quem cria é o dono; ele dá e tira o cargo de administrador. Uma instalação do Syden hospeda várias comunidades, com dois freios (em deploy/.env) para o consumo não fugir do controle: MAX_COMMUNITIES_PER_USER e MAX_MEMBERS_PER_COMMUNITY.README 전문 보기 ↗
Um app estilo Discord para a galera: comunidades com canais de texto, salas de voz, câmera e compartilhamento de tela.
Cada comunidade é um "servidor" no sentido do Discord: tem os próprios canais, membros, emojis, sons e
cargos, e entra-se nela por um código de convite. Quem cria é o dono; ele dá e tira o cargo de administrador.
Uma instalação do Syden hospeda várias comunidades, com dois freios (em deploy/.env) para o consumo não fugir
do controle: MAX_COMMUNITIES_PER_USER e MAX_MEMBERS_PER_COMMUNITY.
| Parte | Tecnologia | Onde roda |
|---|---|---|
web/ | React + Vite + LiveKit Components | GitHub Pages |
desktop/ | Electron (o mesmo que o Discord usa) | PC de cada pessoa (instalador .exe) |
server/ | Node 24 + Fastify + Socket.IO + SQLite | VPS na nuvem (Hetzner) |
| Mídia (voz/vídeo/tela) | LiveKit (SFU open source) | VPS na nuvem (Hetzner) |
Como funciona: o navegador faz login no server, que emite um token para a sala de voz. Com o token, o navegador
se conecta direto ao LiveKit, que recebe o áudio e o vídeo de cada pessoa uma vez e repassa para os outros
participantes da sala. O chat e o "quem está em cada sala" passam pelo Socket.IO do server.
Pré-requisitos: Node 24 e Docker Desktop (aberto).
npm install
cp server/.env.example server/.env
npm run dev:livekit # terminal 1: servidor de mídia (Docker)
npm run dev:server # terminal 2: API em http://localhost:3001
npm run dev:web # terminal 3: site em http://localhost:5173
Crie uma conta com o código de convite amigos (definido em server/.env). Para testar a voz sozinho, abra
duas janelas (uma delas anônima) com contas diferentes.
Não existe ambiente de teste separado, e é decisão consciente: uma segunda VPS custaria dinheiro e manutenção para um app deste tamanho. Em troca, duas precauções cobrem as duas classes de problema que a máquina de desenvolvimento não pega, porque são do ambiente e não do código:
npm test -w server # 32 testes, ~5 s, sem precisar de Docker nem de porta livre
npm run typecheck -w server
Os testes do servidor sobem a API inteira em memória (buildApp), com um banco SQLite novo em pasta
temporária, e batem nas rotas de verdade com app.inject(). Cobrem o que quebra em silêncio: quem pode ler
o quê, o freio contra tentativa de senha, e o que derruba uma sessão. Rodam também a cada push, no
workflow Verificar, junto com a compilação do site e o npm audit.
Os testes de ponta a ponta (e2e/) abrem o Syden num navegador de verdade e precisam da API e do site no ar;
veja o e2e/LEIA.md.
Para testar com alguém na mesma rede Wi-Fi, suba o LiveKit com o IP do seu PC na rede:
LIVEKIT_NODE_IP=192.168.0.10 npm run dev:livekit. Lembre que o navegador só libera o microfone em
localhost ou em HTTPS, então o teste com amigos de verdade é mais fácil já na VPS.
Qualquer VPS Linux com Docker serve. O Syden roda na Hetzner; a Oracle Cloud (plano grátis) também funciona e está descrita no fim desta seção.
Portas que precisam estar abertas para a internet: TCP 22 (SSH), 80 e 443 (HTTPS), 7881 (mídia via
TCP) e UDP 50000-60000 (mídia).
TRAFFIC_ALLOWANCE_GB do deploy/.env.Aponte dois subdomínios para o IP público, com registro DNS A: api.syden.chat e live.syden.chat.
Sem domínio próprio, o DuckDNS dá subdomínios grátis.
Na Cloudflare, os dois têm que ficar em "DNS only" (nuvem cinza). Com o proxy ligado (nuvem laranja) a chamada de voz não conecta: a mídia do LiveKit é UDP, e o proxy da Cloudflare não repassa UDP. O certificado continua sendo emitido pelo Caddy, como sempre.
No servidor:
curl -fsSL https://get.docker.com | sudo sh
git clone https://github.com/<usuario>/<repo>.git janja && cd janja/deploy
cp .env.example .env && nano .env # preencha tudo
sudo docker compose up -d --build --wait
Confira em https://api.syden.chat/api/health. A resposta deve ser {"ok":true}.
Tem datacenter em São Paulo e ~10 TB de tráfego por mês, mas a configuração é mais trabalhosa.
sudo iptables -I INPUT 5 -p tcp -m multiport --dports 80,443,7881 -j ACCEPT
sudo iptables -I INPUT 5 -p udp --dport 50000:60000 -j ACCEPT
sudo netfilter-persistent save
A Oracle pode desligar instâncias grátis ociosas. Para evitar, converta a conta para Pay As You Go (os recursos Always Free continuam sem custo) e crie um alerta de orçamento de US$1 em Billing → Budgets.
VITE_API_URL com
https://api.syden.chat.main (ou rode o workflow manualmente). O site fica em https://<usuario>.github.io/<repo>/.CORS_ORIGIN no deploy/.env é https://<usuario>.github.io.O Syden funciona sem isto: com RESEND_API_KEY vazia, as mensagens vão para o registro do servidor em
vez de saírem. Mas sem envio de verdade, quem esquece a senha perde a conta — não há para onde mandar
o link.
Crie a chave em resend.com → API Keys, com permissão apenas de envio (Sending access). Ela aparece uma vez só; copie na hora.
Em Domains → Add Domain, cadastre syden.chat. O Resend devolve três registros de DNS (um MX e
dois TXT, para SPF e DKIM). Copie os três no painel da Cloudflare, em DNS → Records.
Espere o domínio ficar Verified no Resend. Costuma levar de alguns minutos a algumas horas.
Só então troque o remetente no deploy/.env:
EMAIL_FROM=Syden <[email protected]>
SITE_URL=https://syden.chat/app # ONDE O SYDEN MORA, com o /app: é daqui que saem os
# links dos e-mails e para cá volta a entrada social
Antes da verificação, o remetente de teste do Resend só entrega para o e-mail dono da conta. Um amigo que pedir recuperação de senha não recebe nada — e não vê erro nenhum, porque a tela nunca conta se o endereço existe. Por isso: não anuncie a recuperação de senha antes de o domínio estar verificado.
Sem isto o Syden funciona igual: o botão de GIF simplesmente não aparece na barra de mensagem, e ninguém vê um botão que não pode funcionar.
Crie uma conta em developers.giphy.com → Create an App → escolha API (não SDK). A chave sai na hora, de graça.
Ponha no deploy/.env:
GIPHY_API_KEY=a-chave-que-eles-deram
Reinicie o servidor. O botão aparece para todo mundo no mesmo instante — não precisa de versão nova do aplicativo.
Três coisas que valem saber:
server/src/gifs.ts).web/site/privacidade.html, seção "GIPHY"), e essa declaração é a CONDIÇÃO para o
domínio deles estar liberado na política de segurança — a mesma regra que vale para a medição da
Cloudflare.Para ver a tela antes de ter chave, há uma costura de desenvolvimento: GIFS_ENDERECO aponta a busca
para outro servidor (ver e2e/gifs.mjs). Em produção, não escreva essa variável.
O servidor avisa sozinho por e-mail quem administra (endereço confirmado) quando a voz cai, quando há rajada de erros ou quando ler um arquivo começa a travar o processo. Tem um limite conhecido, e é bom ser explícito sobre ele: servidor morto não manda e-mail.
Para saber que a máquina inteira caiu, é preciso alguém de fora cutucando. Configure um serviço de ping gratuito — o UptimeRobot leva cinco minutos — apontando para:
https://api.syden.chat/api/health
Ele responde {"ok":true} e não exige login, justamente para servir de sinal de vida. Peça alerta por
e-mail a cada 5 minutos de silêncio.
O app é uma janela do Electron que carrega o site do GitHub Pages. Por isso, as atualizações do site chegam
para todos sem reinstalar nada. Só é preciso gerar um instalador novo quando algo muda na pasta desktop/.
Em relação ao navegador, o app de desktop acrescenta:
Ctrl+R (recarregar) e Ctrl+Shift+I (ferramentas de desenvolvedor).Para testar no seu PC com o ambiente de desenvolvimento rodando: npm run dev:desktop.
Para publicar:
git tag v0.1.0 && git push --tags.Syden-Setup.exe e o anexa a uma Release do GitHub.https://github.com/<usuario>/<repo>/releases/latest/download/Syden-Setup.exe. Para usar outro, crie a
variável DESKTOP_DOWNLOAD_URL no repositório. O botão só aparece no navegador do Windows (não dentro do app).
Enquanto não existir nenhuma Release, o link dá erro 404, então publique a primeira antes de divulgar o site.https://<usuario>.github.io/<repo>/. Para usar outro endereço, crie a variável
JANJA_URL no repositório.Para gerar o instalador localmente, ajuste a URL em desktop/app.config.json e rode npm run dist:desktop.
O arquivo sai em desktop/release/.
O site também se instala como programa: no Edge ou no Chrome aparece Instalar na barra de endereço, e a tela de login tem o botão Instalar o Syden. Vira uma janela própria, com ícone no menu iniciar, sem download de instalador e sem o aviso do Windows para programas sem assinatura. Ficam de fora os atalhos globais de microfone, o ícone na bandeja e continuar aberto em segundo plano — para isso, o instalador.
O web/public/sw.js existe só porque o navegador exige um service worker para oferecer a instalação; ele não
guarda nada em cache, então ninguém fica preso numa versão antiga.
O instalador não tem assinatura digital, então o Windows mostra "O Windows protegeu o computador" na primeira vez. É só clicar em Mais informações → Executar assim mesmo. Para remover o aviso, é preciso um certificado de assinatura de código, que é pago.
Site: basta dar push, o GitHub Actions publica.
Servidor: git pull && sudo docker compose up -d --build --wait && sudo docker builder prune -f --filter until=168h && sudo docker image prune -f
dentro de deploy/ (no servidor do Syden, /opt/janja/deploy). Com o cofre, nada muda no deploy; só depois de a MÁQUINA
reiniciar é preciso sudo ./cofre-abrir.sh antes (ver docs/cofre.md).
O builder prune no fim também não é enfeite. Cada --build guarda no cache as camadas da versão anterior, e
nada as apaga sozinho: em 17/10/2026 eram 5,1 GB de cache contra 16 MB de dados das pessoas. O filtro apaga só o que
tem mais de uma semana, então o deploy seguinte continua rápido. O image prune apaga as imagens que cada --build
deixa para trás, sem uso. Não use docker system prune --volumes nem docker volume prune: os volumes que sobram são
os do Caddy (o certificado HTTPS), e, num servidor sem o cofre, o banco e os arquivos do Syden.
O --wait não é enfeite. Sem ele o comando devolve o terminal assim que o container é criado,
e um servidor que sobe e morre em seguida (um .env errado, por exemplo) passa despercebido: você
fecha o terminal achando que deu certo e descobre pelo amigo no dia seguinte. Com ele, o comando só
termina depois que o servidor novo responde de verdade — e falha na sua cara se não responder.
Vale saber o que acontece nesse comando, porque assusta menos do que parece: a construção inteira roda com o servidor velho no ar, e só a troca no fim derruba a API — por volta de dois segundos. A voz não cai junto, porque ela passa pelo LiveKit, que é outro container e não é reiniciado. O site mostra "Reconectando ao servidor…" e volta sozinho. O que falha nesses dois segundos é mensagem enviada na hora exata e quem tentar ENTRAR numa sala de voz (a senha de entrada vem da API).
Para conferir depois, sem entrar no servidor: curl -s https://api.syden.chat/api/inicio traz
construidoEm, que é quando a imagem no ar foi construída.
Backup: são duas coisas, e desde que os arquivos saíram do banco elas mudam em ritmos diferentes.
No volume janja_data ficam janja.db (as fichas — pequeno, muda a toda hora) e arquivos/ (o que as
pessoas subiram — grande, quase nunca muda). Copiar só o banco perde os avatares; copiar só os
arquivos perde tudo o mais. Um arquivo em arquivos/ nunca é reescrito — o nome dele é o hash do
conteúdo —, então cópia incremental funciona bem.
O cofre: em produção, o banco e os arquivos moram num disco cifrado que só abre com a senha, digitada depois de cada reinício da máquina (, uma vez; , a cada reinício). Com ele, no troca o volume pela pasta do cofre. Passo a passo em .
Os administradores veem, em Uso do servidor (topo da barra lateral):
Não precisa configurar nada. O próprio servidor mede os bytes enviados pela máquina (contadores do Linux) e
compara com a franquia definida em TRAFFIC_ALLOWANCE_GB, no deploy/.env. Coloque ali a franquia do seu plano;
o padrão, 10 TB, é o da Oracle. A medição começa quando o servidor é instalado; o painel avisa se ela começou no
meio do mês.
O tráfego que importa é o que sai do LiveKit: cada stream é copiado para cada pessoa que assiste.
| Situação | Saída aproximada |
|---|---|
| 10 pessoas em voz (~40 kbps cada) | ~4 Mbps |
| 1 tela 1080p30 (até 5 Mbps) assistida por 9 | até ~45 Mbps |
Uma VPS pequena aguenta isso com folga.
ISTO ERA O GARGALO, E FOI RESOLVIDO EM 2026-10-01 — os arquivos saíram do banco e foram para o
disco (server/src/arquivos.ts). A tabela abaixo é o que acontecia ANTES, e fica aqui porque é a
medição que justificou a mudança. O node:sqlite é síncrono: enquanto o servidor lia um BLOB, o
processo inteiro parava — nada de voz, nada de chat, para ninguém. npm run medir:blobs media isso:
| Arquivo | Servidor travado em 1 leitura | Em 10 leituras seguidas |
|---|---|---|
| Avatar (50 KB) | 0,1 ms | 0,9 ms |
| Emoji grande (0,5 MB) | 0,7 ms | 6 ms |
| Foto (2 MB) | 3,6 ms | 28 ms |
| Clipe curto (8 MB) | 12 ms | 115 ms |
| Recado em vídeo (20 MB) | 28 ms | 288 ms |
Lendo assim: um recado em vídeo de 20 MB deixa o servidor surdo por ~28 ms. A voz começa a engasgar quando isso passa de uns 50 a 100 ms, então três ou quatro vídeos pedidos ao mesmo tempo já dão para ouvir. Com dez amigos nunca acontece; com cem pessoas ativas, acontece todo dia.
O que foi feito: os bytes moram em arquivos/, ao lado do banco, com o nome de cada arquivo
sendo o sha256 do próprio conteúdo. O servidor não lê mais o arquivo para a memória — ele entrega do
disco —, e passou a responder a pedido de pedaço (Range), que é o que faz arrastar a barra de uma
música do karaokê pedir só o trecho em vez de rebaixar a música inteira.
A migração foi feita com 96 arquivos e 6 MB, de propósito: mover noventa e seis arquivos é uma tarde, mover cinquenta mil é um plano de manutenção.
# dentro de deploy/, com o servidor já atualizado
sudo docker compose exec api node scripts/mover-arquivos.mjs --ver # quanto falta
sudo docker compose exec api node scripts/mover-arquivos.mjs # move (pode rodar com o Syden no ar)
sudo docker compose exec api node scripts/mover-arquivos.mjs --conferir # cada linha tem mesmo o arquivo?
sudo docker compose exec api node scripts/limpar-orfaos.mjs # arquivo sem dono (lista; --apagar apaga)
O passo seguinte, quando um disco só não bastar, é um armazenamento de objetos (Cloudflare R2 ou S3)
com o navegador baixando direto de lá. Com o arquivo já fora do banco e endereçado por hash, isso vira
trocar onde arquivos.ts lê e escreve.
Caminho para crescer, na ordem:
redis: no livekit.yaml).server. Trocar SQLite por Postgres, usar @socket.io/redis-adapter e mover o estado de voz em
memória (server/src/realtime.ts) para o Redis.deploy/cofre-criar.shdeploy/cofre-abrir.shDADOS=/srv/syden-cofre/dados.env