Synaplan
开源 AI 软件平台,面向企业:自托管 AI 代理,符合 GDPR 的聊天机器人小部件,RAG 知识库,WhatsApp 与 Outlook AI,MCP,多模型路由。支持 Docker、Kubernetes 或在 Germ 托管。
AI 翻译
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
CRM multi-tenant para imobiliárias com distribuição automática de leads do Meta Lead Ads: webhook → roleta em round-robin entre os corretores (1º lead novo vai pro corretor #1, o 2º pro #2, o 3º pro #3, e assim por diante — cada lead que expira sem resposta escala pro próximo colocado, mas isso não afeta pra onde o próximo lead NOVO vai) → temporizador real controlado pelo servidor → transferência automática se ninguém responder a tempo → aviso por WhatsApp ao corretor (novo lead e prazo esgotado) → dashboards em tempo (quase) real para dono e corretores.查看完整 README ↗
CRM multi-tenant para imobiliárias com distribuição automática de leads do Meta Lead Ads: webhook → roleta em round-robin entre os corretores (1º lead novo vai pro corretor #1, o 2º pro #2, o 3º pro #3, e assim por diante — cada lead que expira sem resposta escala pro próximo colocado, mas isso não afeta pra onde o próximo lead NOVO vai) → temporizador real controlado pelo servidor → transferência automática se ninguém responder a tempo → aviso por WhatsApp ao corretor (novo lead e prazo esgotado) → dashboards em tempo (quase) real para dono e corretores.
Identidade visual: preto (#191919) + dourado (#F6C324) + off-white (#FFFFF0).
| Camada | Escolha | Observação |
|---|---|---|
| Frontend/backend | Next.js 16 (App Router) + TypeScript + Tailwind v4 | API Routes fazem a autoridade de negócio; nada crítico roda só no cliente |
| Banco | PostgreSQL no Railway | Não é Supabase — decisão do usuário. Prisma com migrations reais |
| Auth | Auth.js v5 (Credentials + JWT) + bcrypt | Sem Supabase Auth disponível |
| Multi-tenant | src/lib/tenant-db.ts — extensão do Prisma Client que injeta organization_id (vindo da sessão) em toda query | Substitui o RLS nativo do Supabase; nunca confia só no frontend |
| Realtime | Polling curto (2–3s) via SWR | Sem Supabase Realtime; simples, confiável, sem infra extra. Ver "Evoluindo o realtime" abaixo |
| Job de expiração | Processo Node separado em worker/, rodando a cada 15s | Plano Hobby da Vercel só permite cron 1x/dia — insuficiente para expirar leads em minutos. O worker roda no Railway (junto do banco), sempre ativo |
| Deploy | App → Vercel · Banco + worker → Railway |
Toda a lógica crítica (quem recebe o lead, quando expira, quem pode assumir) vive em
packages/db/src/rotation.ts, compartilhada entre o app Next.js e o worker — é a única fonte
de verdade, com transações e SELECT ... FOR UPDATE para nunca permitir duas pessoas
assumirem o mesmo lead (ver "Concorrência" abaixo).
app/ Next.js — UI, API routes, auth, middleware
worker/ Processo Node que expira atribuições vencidas (roda continuamente)
packages/db/ Schema Prisma, migrations, seed, motor de rotação (fonte única de verdade)
Pré-requisitos: Node 20+, um banco Postgres acessível publicamente (ex.: Railway).
npm install
+ New → Database → Add PostgreSQL).DATABASE_PUBLIC_URL (host termina em *.proxy.rlwy.net). É essa URL
pública que você vai usar — a DATABASE_URL interna (*.railway.internal) só funciona
entre serviços dentro do próprio projeto Railway.${{...}}) da aba Variables.cp packages/db/.env.example packages/db/.env
cp worker/.env.example worker/.env
cp app/.env.example app/.env.local
Preencha DATABASE_URL nos três com a mesma connection string pública do Railway. Gere os
segredos indicados nos comentários do app/.env.example (AUTH_SECRET,
META_TOKEN_ENCRYPTION_KEY, META_VERIFY_TOKEN, CRON_SECRET).
npm run db:migrate # cria as tabelas (prisma migrate dev)
npm run db:seed # popula um ambiente de demonstração (ver contas abaixo)
O seed é isolado (slug: prospera) e nunca deve ser usado em produção — ver seção
"Seed vs. produção".
npm run dev # Next.js em http://localhost:3000
npm run worker # em outro terminal — processa expirações a cada 15s
Sem o worker rodando, leads continuam sendo criados e atribuídos normalmente, mas nunca expiram/transferem sozinhos — para produção ele precisa estar sempre ativo (ver "Deploy").
As senhas dos corretores são geradas aleatoriamente pelo seed e impressas no terminal a cada
execução (npm run db:seed) — anote a saída do comando. Valores atuais:
| Papel | Senha | |
|---|---|---|
| Dono (OWNER) | [email protected] | metodoneosprospera2026 |
| Corretor #1 | [email protected] | bCGPtopWA5 |
| Corretor #2 | [email protected] | 9g3czG86Uj |
| Corretor #3 | [email protected] | ovdPD4kr6t |
| Corretor #4 | [email protected] | vwREywYudJ |
| Corretor #5 | [email protected] | btPuDeazXB |
| Corretor #6 | [email protected] | LyHX53sLpj |
Importante: essas senhas estão em texto puro em packages/db/seed.ts e agora no histórico do
git — são credenciais reais de uso, não só dados de demonstração. Troque-as após o primeiro
login (ainda não há tela de "alterar senha" — pode ser feito gerando um novo hash com bcrypt e
atualizando password_hash diretamente, ou recriando o usuário) antes de expor este ambiente
publicamente, e evite dar push deste repositório para um remoto público sem antes trocar a
senha do OWNER.
npm test --workspace app
Roda contra o Postgres real configurado em app/.env.local (cada teste cria sua própria
organização isolada e limpa tudo ao final — não usa o mesmo banco de dev de forma destrutiva,
mas evite rodar contra produção). Cobre os cenários críticos do escopo original: atribuição ao
primeiro corretor, timer gravado pelo servidor, claim dentro do prazo interrompe a rotação,
expiração transfere para o próximo, corretor pausado é pulado, ciclo completo volta ao
primeiro corretor, dois cliques simultâneos só um vence, corretor errado nunca assume,
idempotência do meta_lead_id, isolação cross-tenant, organização sem corretor ativo, e o
webhook do Meta de ponta a ponta (assinatura, verificação, criação, idempotência) com a Graph
API mockada no formato oficial documentado.
Todo o ciclo de vida de uma atribuição (lead_assignments) passa por transações Postgres com
SELECT ... FOR UPDATE na linha ativa antes de qualquer leitura/decisão:
claimLead em packages/db/src/rotation.ts): trava a tentativa ASSIGNED atual,
revalida corretor + prazo, só então marca CONTACTED.expireAndRotate, chamado pelo worker a cada 15s): trava a mesma linha,
revalida status = ASSIGNED AND expires_at <= now() antes de expirar — se um claim já
resolveu a tentativa, não faz nada.assignNextLead/distributeNewLead): trava a linha de rotation_state da
organização antes de ler/avançar a posição da fila, serializando leads concorrentes.O timer nunca é decidido pelo navegador: expires_at é gravado pelo servidor no momento da
atribuição, e o frontend só calcula expires_at - hora_do_servidor (recebida a cada
resposta da API) para exibir a contagem regressiva.
Pesquisado na documentação oficial atual (developers.facebook.com) antes de implementar —
não foi assumido nada de versões antigas da API.
https://SEU_DOMINIO/api/webhooks/metaMETA_VERIFY_TOKEN no seu .envleadgenleads_retrieval, pages_manage_metadata, pages_show_list,
pages_read_engagement, ads_management (e pages_manage_ads se for usar a leitura
completa de campanha via Graph API)./settings/integrations/meta, informe o Page ID e o token de
acesso da página. O CRM automaticamente:
GET /{page-id}),POST /{page-id}/subscribed_apps?subscribed_fields=leadgen),/leads que ele chegou e foi distribuído.{ object: "page", entry: [{ id: <page_id>, changes: [{ field: "leadgen", value: { leadgen_id, page_id, form_id, adgroup_id, ad_id, created_time } }] }] }.
campaign_id não vem no webhook nem no lead — o CRM busca via ad_id (GET /{ad_id}?fields=name,campaign{id,name},adset{id,name}), de forma best-effort (se falhar, o
lead é salvo mesmo assim, só sem esses nomes).X-Hub-Signature-256: sha256=<hmac> (HMAC-SHA256 do corpo bruto com o App
Secret) — validado em toda requisição antes de processar.GET /{leadgen_id}?fields=id,created_time,ad_id,form_id,field_data —
field_data é um array de { name, values: [...] } (uma pergunta do formulário por item).meta_lead_id é UNIQUE no banco — reenvios do mesmo evento (a Meta reenvia em
caso de falha de ack por até 36h) nunca duplicam o lead; o CRM registra um evento
WEBHOOK_DUPLICATE e não redistribui.Usa a WhatsApp Cloud API oficial da Meta (gratuita dentro do volume normal de uma operação pequena/média, sem risco de banimento de número — ao contrário de bibliotecas não-oficiais que imitam o WhatsApp Web). Três mensagens automáticas possíveis por lead: (1) quando ele é atribuído a um corretor pela primeira vez, (2) quando esse corretor deixa o prazo esgotar sem responder, e (3) se a roleta der uma volta completa sem ninguém responder e o lead voltar para um corretor que já tinha recebido esse mesmo lead antes — nesse caso é um template diferente ("já passou pela equipe toda"), não uma repetição do aviso de "novo lead" a cada volta.
/settings/integrations/whatsapp:
novo_lead_atribuido: "Olá {{1}}! Você recebeu um novo lead no CRM Próspera: {{2}},
telefone {{3}}. Você tem {{4}} minutos para entrar em contato antes que ele passe para o
próximo corretor."lead_expirado_corretor: "Atenção {{1}}: o tempo para atender o lead {{2}} esgotou e ele
foi transferido para o próximo corretor da fila."lead_retornou_corretor: "Atenção {{1}}! O lead {{2}} (telefone {{3}}) já passou por toda
a equipe sem resposta e voltou para você. Você tem mais {{4}} minutos para entrar em
contato." — enviado no lugar de novo_lead_atribuido quando a roleta já tinha passado por
esse corretor antes para o mesmo lead (evita repetir a mesma mensagem a cada volta).
A aprovação pode levar de minutos a alguns dias; enquanto pendente, o envio falha e fica
registrado em audit_logs/no campo "Último erro" da integração, sem travar a distribuição
do lead (é best-effort).whatsapp_business_messaging (o mesmo System User usado
para os Leads pode ganhar essa permissão extra)./settings/integrations/whatsapp, informe o Phone Number ID e o
token. O CRM testa (GET /{phone-number-id}) e salva o token criptografado./settings/brokers (campo já existente)
— é para esse número que as mensagens são enviadas.Web Push de verdade (Service Worker + Push API + VAPID) — sem Firebase, sem OneSignal, sem
custo. Avisa todos os OWNER/ADMIN e todo corretor com status = ACTIVE assim que um lead
novo é criado (webhook do Meta ou criação manual), mesmo com a aba/app fechado, incluindo
celular (Android e iPhone via PWA instalado) e PC.
push_subscriptions (packages/db/prisma/schema.prisma) — um usuário pode ter várias
inscrições (uma por navegador/dispositivo). Migration: add_push_subscriptions.app/src/lib/push-server.ts — notifyNewLead(lead) (chamada nos dois pontos reais de criação
de lead: api/webhooks/meta e api/leads, via after() do Next.js — não atrasa nem quebra a
resposta) e sendTestPush(userId). Limpa sozinho inscrições que voltarem 404/410 (dispositivo
desinstalou/expirou); qualquer outro erro só fica registrado em lastError, nunca derruba o
envio dos demais.app/public/sw.js (Service Worker) + app/public/manifest.json + ícones (icon-192.png,
icon-512.png, icon-512-maskable.png, apple-touch-icon.png, gerados a partir do símbolo
do logo da Próspera).GET /api/push/vapid-public-key (pública), POST /api/push/subscribe,
POST /api/push/unsubscribe, GET /api/push/subscriptions, DELETE /api/push/subscriptions/:id,
POST /api/push/test (só OWNER/ADMIN)./settings/notifications (dono) e /broker/profile (corretor), com
lista de dispositivos, botão de desativar por dispositivo e botão de teste (admin).node -e "console.log(JSON.stringify(require('web-push').generateVAPIDKeys()))"
Adicione na Vercel (Production) e no .env.local:
VAPID_PUBLIC_KEY="..."
VAPID_PRIVATE_KEY="..."
VAPID_SUBJECT="mailto:[email protected]"
VAPID_SUBJECT precisa ser uma URI mailto: ou https: válida — alguns serviços de push
rejeitam a requisição sem isso. Só o app (Vercel) precisa dessas variáveis — o worker não envia
push, só a Meta/WhatsApp.
/settings/notifications.localhost não basta — o celular batendo no seu PC
pela rede local não é um "contexto seguro" pro navegador; use um deploy de preview da Vercel
ou um túnel HTTPS (ngrok, Tailscale Funnel) em vez disso.Motivado por um incidente real: a assinatura Pro da Vercel foi suspensa por uma falha de
cobrança e o site ficou fora do ar (402 Deployment Disabled) sem que ninguém percebesse na
hora. Esse healthcheck roda de propósito no worker (Railway), não no app (Vercel) — se a
Vercel cair ou for suspensa, algo hospedado nela não conseguiria avisar ninguém.
worker/src/healthcheck.ts: a cada HEALTHCHECK_INTERVAL_SECONDS (padrão 60s), faz um
GET em HEALTHCHECK_URL (padrão a própria URL de produção). Considera "fora do ar" só
402 (suspensão de billing — o caso real que motivou isso), 5xx ou falha de conexão —
qualquer outra resposta (200, redirect pro login, até um 404) conta como "no ar", porque
isso não é um teste de corretude da aplicação, só de disponibilidade.HEALTHCHECK_FAILURE_THRESHOLD checagens
seguidas com falha (padrão 3, ~3 minutos), manda um WhatsApp; e manda outro quando voltar ao
normal, com o tempo total de indisponibilidade. Nunca manda uma mensagem por checagem — senão
vira spam a cada minuto enquanto durar a queda./settings/integrations/whatsapp (mesmo
número usado pros avisos de lead) — não precisa configurar um número separado.crm_sistema_fora_do_ar: "⚠️ Alerta CRM Próspera: o sistema está fora do ar desde {{1}}.
Motivo detectado: {{2}}."crm_sistema_recuperado: "✅ CRM Próspera voltou ao normal às {{1}}, após {{2}} fora do ar."
A aprovação pode levar de minutos a alguns dias; enquanto pendente, o envio do alerta falha
silenciosamente (só fica no log do worker), sem derrubar o healthcheck em si.HEALTHCHECK_URL="https://crmprospera.vercel.app"
HEALTHCHECK_INTERVAL_SECONDS=60
HEALTHCHECK_FAILURE_THRESHOLD=3
HEALTHCHECK_ALERT_PHONE="+5547997580595"
Deixe HEALTHCHECK_ALERT_PHONE vazio para desativar o healthcheck sem remover o código.OWNER, ADMIN, BROKER. Middleware (src/middleware.ts) redireciona por role;
toda rota de API revalida com requireSession([...roles]).scopedDb(organizationId), que injeta o filtro a partir da sessão; (2) o motor de
rotação/claim/expiração (código de sistema, não uma requisição de usuário) sempre recebe
organizationId explícito e o valida em toda escrita.META_TOKEN_ENCRYPTION_KEY, compartilhada entre app e worker) antes de salvar; nunca
expostos ao frontend.AUTH_SECRET)./api/cron/expire-assignments (fallback de teste do worker) exige
Authorization: Bearer $CRON_SECRET.App (Vercel): importe o repositório, defina o root directory como app, configure as
mesmas variáveis de app/.env.example no painel da Vercel (DATABASE_URL apontando para o
Postgres do Railway). Depois do deploy, atualize a URL do webhook no App Dashboard da Meta para
o domínio de produção.
Worker (Railway): crie um novo serviço no mesmo projeto Railway a partir deste repositório,
root directory worker, comando de start npm run start --workspace worker (ou
node --import tsx src/index.ts), com DATABASE_URL apontando para o mesmo Postgres e
META_TOKEN_ENCRYPTION_KEY com o mesmo valor usado na Vercel (o worker precisa decifrar
tokens de integrações — Meta e WhatsApp — que foram criptografados pelo app). Esse processo
precisa ficar sempre ativo — é ele quem expira e transfere leads (e dispara o aviso de
"prazo esgotado" por WhatsApp) sem depender do navegador de ninguém estar aberto (seção "o
sistema continua funcionando mesmo com o navegador fechado").
npm run backup (na raiz) gera um arquivo crm-prospera-backup-AAAA-MM-DD_HH-mm.json.gz com
todas as tabelas e registros do banco (as tabelas são descobertas pelo catálogo do Postgres,
então tabelas novas entram sozinhas). O script só lê: roda numa transação READ ONLY +
REPEATABLE READ (foto consistente) e, depois de gravar, relê o arquivo e confere a contagem de
cada tabela antes de dar o backup como válido.
Documentos\Backups CRM Prospera e G:\Meu Drive\Backups CRM Prospera (o Google
Drive para computador sincroniza sozinho com a nuvem). Outros destinos: variável
BACKUP_DIRS (separados por ;).packages/db/scripts/backup-diario.cmd). Se o computador estiver desligado nesse
horário, roda assim que ele ligar.ULTIMO-BACKUP.txt (data, tamanho, contagens) e backup.log
(na pasta de Documentos). Se um backup falhar, aparece BACKUP-COM-ERRO.txt com o motivo.Restaurar (sempre num banco novo e vazio — o script recusa banco que já tenha dados e faz tudo numa transação única, conferindo as contagens no final):
cd packages/db
DATABASE_URL="<url-do-banco-novo>" npx prisma migrate deploy # cria as tabelas (banco em UTF8)
npm run restore-backup -- --file "<arquivo.json.gz>" --target "<url-do-banco-novo>"
O polling (2–3s) atende ao requisito de "sem refresh manual", mas para push instantâneo no
futuro: um serviço WebSocket dedicado no Railway (mesma vantagem de long-running process que o
worker já usa) ou um provedor gerenciado como Pusher/Ably, publicando eventos a partir dos
mesmos pontos onde hoje gravamos audit_logs (assign/claim/expire).
packages/db/seed.ts cria a organização prospera com dados fictícios — nunca é
chamado pelo fluxo principal (webhook, API de criação de corretor, etc.) e é seguro rodar em
desenvolvimento repetidamente (idempotente). Não rode npm run db:seed contra um banco de
produção com dados reais.
audit_logs já registra todos os eventos
necessários para disparar isso depois).organization_id de um jeito
que comporta uma tabela teams no meio sem quebrar nada, mas isso não foi implementado.