Lalabase
Tickets, Doku, Zeiterfassung und Kundengespräche an einem Ort, durchsuchbar per KI. Für Softwarefirmen mit Kundenprojekten. Open Source: auf deinem eigenen Server oder in unserer Cloud in Deutschland.
Translation pending
Distribuição automática de leads do Meta Ads para corretores
Translation pending
BusinessTypeScript · JavaScriptGitHub @neosdigitalOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
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
Loading the full README. The saved excerpt is shown below.
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.Read the full 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.