Voz, vídeo, tela compartilhada e conversa com os amigos. De graça.
Translation pending
SocialTypeScript · HTMLGitHub @SydenstrickerOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Voz, vídeo, transmissão de tela e chat para grupos de amigos. Sem anúncios e sem assinatura.
Translation pending
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
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.Read the full 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