CRM web integrado ao WhatsApp, distribuído como SaaS multi-tenant. Permite que profissionais autônomos e pequenas empresas gerenciem conversas de WhatsApp, contatos e relacionamento comercial em um único painel — sem depender do WhatsApp Web.
O projeto nasce da necessidade de centralizar o atendimento via WhatsApp com contexto de CRM: quem é o cliente, a qual empresa ele pertence, qual o histórico de conversas e quais compromissos estão pendentes. Tudo isso acessível em uma interface construída para uso profissional.
Público-alvo: profissionais autônomos, consultores, pequenas empresas que usam WhatsApp como canal principal de atendimento.
Modelo de distribuição: SaaS multi-tenant — cada organização tem seus dados completamente isolados.
| Módulo | Status |
|---|---|
| Autenticação com email e senha | Concluído |
| Caixa de entrada WhatsApp em tempo real (SSE) | Concluído |
| Envio de mensagens via WhatsApp | Concluído |
| Clientes — CRUD + entrada automática via WhatsApp | Concluído |
| Empresas — CRUD + vinculação com clientes | Concluído |
| Lembretes — CRUD + vinculação com clientes ou empresas | Concluído |
| Webhook processor (recebimento de mensagens) | Concluído |
| Conexão WhatsApp via QR Code | Bloqueado — ver seção de status |
| Tela de detalhe de empresa | Pendente |
| Formulário de criação de lembretes no frontend | Pendente |
| Deploy final | Pendente |
┌─────────────────────────────────────────────────────┐
│ Usuário (Browser) │
│ React + Vite + TypeScript │
└─────────────────────┬───────────────────────────────┘
│ HTTP / SSE
▼
┌─────────────────────────────────────────────────────┐
│ Backend Express (Node.js) │
│ │
│ ┌──────────────┐ ┌────────────┐ ┌─────────────┐ │
│ │ Auth.js v5 │ │ REST API │ │ SSE /events│ │
│ └──────────────┘ └─────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ┌────────────────────────▼────────────────▼──────┐ │
│ │ Middleware de Tenant (organization_id) │ │
│ └────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────────────▼────────────────────────┐ │
│ │ Neon PostgreSQL (postgres.js) │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────┐ │
│ │ POST /webhooks/evolution ← Evolution API │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│ HTTP
▼
┌─────────────────────────────────────────────────────┐
│ Evolution API (self-hosted) │
│ Uma instância por organização │
│ Conectada ao WhatsApp via QR Code │
└─────────────────────────────────────────────────────┘
WhatsApp → Evolution API → POST /webhooks/evolution
→ valida X-Evolution-Secret
→ identifica tenant pelo instanceName
→ upsert cliente (cria se novo)
→ upsert conversa
→ insere mensagem (deduplicação por whatsapp_message_id)
→ atualiza last_message_at e unread_count
→ emite evento SSE para o frontend
Usuário → POST /api/v1/messages
→ valida JWT + extrai organization_id
→ busca credenciais da Evolution API do tenant
→ envia para Evolution API
→ salva no banco (direction: 'out')
→ emite evento SSE
| Camada | Tecnologia |
|---|---|
| Frontend | React 18 + Vite + TypeScript |
| Estilo | Tailwind CSS (dark mode) |
| Estado global | Zustand |
| Backend | Node.js + Express + TypeScript |
| Banco de dados | PostgreSQL serverless (Neon) |
| ORM/Query builder | postgres.js (SQL nativo) |
| Autenticação | Auth.js v5 (cookie-based, Credentials provider) |
| Evolution API (self-hosted) | |
| Tempo real | SSE (Server-Sent Events) |
| Deploy frontend | Vercel |
| Deploy backend | Fly.io |
melao-gestor/
│
├── backend/ # API REST + webhook + SSE
│ └── src/
│ ├── app.ts # Express — registro de rotas e middlewares
│ ├── auth.config.ts # Auth.js v5 — Credentials provider
│ ├── db/
│ │ ├── index.ts # Conexão Neon via postgres.js
│ │ ├── schema.sql # Schema completo (source of truth)
│ │ └── seed.ts # Cria organização + usuário inicial
│ ├── middleware/
│ │ ├── auth.ts # Valida sessão, injeta req.userId
│ │ └── tenant.ts # Injeta req.organizationId
│ ├── routes/
│ │ ├── conversations.ts # GET / GET /:id POST /:id/read
│ │ ├── messages.ts # POST / — envia via Evolution API
│ │ ├── clients.ts # CRUD + ?search= e ?company_id=
│ │ ├── companies.ts # CRUD + contatos internos + lembretes
│ │ ├── reminders.ts # CRUD + ?linked_type= e ?linked_id=
│ │ ├── webhooks.ts # POST /evolution — recebe mensagens
│ │ └── events.ts # GET / — stream SSE
│ └── services/
│ ├── evolutionApi.ts # Cliente HTTP para a Evolution API
│ ├── sseManager.ts # Gerencia conexões SSE por organização
│ └── webhookProcessor.ts # Processa payload da Evolution API
│
├── frontend/ # SPA React
│ └── src/
│ ├── App.tsx # Rota única /* → AppPage
│ ├── types/index.ts # Interfaces TypeScript do domínio
│ ├── lib/
│ │ ├── api.ts # fetch wrapper com credentials: include
│ │ └── auth.ts # login() / logout()
│ ├── store/
│ │ └── auth.ts # Zustand — usuário persistido
│ ├── pages/
│ │ ├── LoginPage.tsx
│ │ └── AppPage.tsx # Orquestrador — todo o estado aqui
│ └── components/
│ ├── layout/
│ │ ├── AppSidebar.tsx # Sidebar 64px — empresas e navegação
│ │ ├── ClientColumn.tsx # Coluna 240px — lista contextual
│ │ ├── ChatMessages.tsx # Thread de mensagens + input
│ │ └── ClientDetailPanel.tsx # Painel 220px — info do cliente
│ └── clients/
│ ├── NewClientModal.tsx
│ └── NewCompanyModal.tsx
│
└── evolution/ # Evolution API self-hosted
├── Dockerfile # Herda da imagem oficial + entrypoint custom
├── docker-entrypoint.sh # Corrige .env e aplica migrations no startup
└── fly.toml # Configuração Fly.io
Toda tabela de dados de negócio tem organization_id como coluna obrigatória e indexada. Nenhuma query retorna dados sem esse filtro — é a regra de segurança central do projeto.
organizations -- tenant raiz
users -- 1 por organização no MVP
evolution_instances -- credenciais da Evolution API por organização
companies -- empresas contratantes (múltiplas por organização)
company_contacts -- contatos internos de cada empresa
clients -- clientes finais; company_id nullable
conversations -- uma por chat do WhatsApp
messages -- direction: 'in' | 'out'
reminders -- linked_type: 'client' | 'company' | nullTodas as rotas abaixo exigem sessão autenticada via cookie (Auth.js). O organization_id é extraído da sessão pelo middleware de tenant — nunca vem do frontend.
# Autenticação
GET /api/auth/csrf
POST /api/auth/callback/credentials
GET /api/auth/session
POST /api/auth/signout
# Conversas
GET /api/v1/conversations ?page= &limit=
GET /api/v1/conversations/:id
POST /api/v1/conversations/:id/read
# Mensagens
POST /api/v1/messages { conversation_id, content }
# Clientes
GET /api/v1/clients ?search= &company_id= &page= &limit=
GET /api/v1/clients/:id
POST /api/v1/clients
PATCH /api/v1/clients/:id
DELETE /api/v1/clients/:id
# Empresas
GET /api/v1/companies
GET /api/v1/companies/:id retorna + contatos internos + lembretes
POST /api/v1/companies
PATCH /api/v1/companies/:id
DELETE /api/v1/companies/:id
# Lembretes
GET /api/v1/reminders ?linked_type= &linked_id=
POST /api/v1/reminders
PATCH /api/v1/reminders/:id
DELETE /api/v1/reminders/:id
# Webhook (sem JWT — autenticado por secret no header)
POST /webhooks/evolution
# SSE
GET /api/events
O organization_id é embutido no JWT no momento do login. O middleware de tenant o injeta em req.organizationId — toda query usa esse valor como filtro obrigatório.
O que nunca é feito:
- Receber
organization_iddo body ou query params em rotas protegidas - Fazer queries sem
WHERE organization_id = $1 - Retornar 403 (revela existência de dados de outro tenant) — sempre 404
- Node.js 20+
- Conta no Neon (PostgreSQL serverless gratuito)
- Evolution API self-hosted com uma instância configurada
cd backend
npm install
cp .env.example .env # preencher as variáveis
npm run devcd frontend
npm install
cp .env.local.example .env.local # preencher VITE_API_URL
npm run dev# Rodar o schema
psql $DATABASE_URL < backend/src/db/schema.sql
# Criar organização e usuário inicial
cd backend && npx ts-node src/db/seed.tsDATABASE_URL=postgresql://...?sslmode=require
AUTH_SECRET= # openssl rand -base64 32
AUTH_URL=http://localhost:3000
FRONTEND_URL=http://localhost:5173
EVOLUTION_API_URL=https://...
EVOLUTION_API_KEY=
EVOLUTION_WEBHOOK_SECRET= # openssl rand -base64 32
NODE_ENV=development
PORT=3000VITE_API_URL=http://localhost:3000O projeto está com o backend e frontend do MVP praticamente completos. O bloqueio atual está na hospedagem da Evolution API.
Problema: provedores de cloud (Fly.io, AWS, GCP, etc.) têm seus IPs frequentemente bloqueados pelo WhatsApp ao usar o protocolo Baileys (WhatsApp Web). A Evolution API inicia mas não consegue gerar o QR Code de conexão.
Caminho para desbloquear: hospedar a Evolution API em uma VPS com IP não-cloud (Oracle Cloud Always Free, Hetzner, DigitalOcean) ou em um servidor próprio.
- Fase 1 — Fundação: banco, auth, middleware de tenant
- Fase 2A — Evolution API deployada
- Fase 2B — Webhook processor + rotas de backend completas
- Fase 2C — Conexão WhatsApp (bloqueado — ver status acima)
- Fase 3 — Caixa de entrada com SSE
- Fase 4 — Módulo de clientes completo
- Fase 5 — Módulo de empresas (backend completo / UI de detalhe pendente)
- Fase 6 — Módulo de lembretes (backend completo / criação no frontend pendente)
- Fase 7 — Gerenciamento WhatsApp (QR Code, status)
- Fase 8 — Deploy final (Vercel + Fly.io)
- Fase 9 — Inteligência Artificial (resumo de conversa, sugestão de resposta, classificação de urgência)
MIT License — veja o arquivo LICENSE para detalhes.
