amostra 04/em desenvolvimento
Atlas
Um CRM de vendas que virou um sistema de gestão para pequenos negócios, moldado ao setor de cada empresa.
- Next
- Prisma
- Zod
- Turbo
01
Contexto
O Atlas começou como um CRM para uma equipe de vendas: funil por status, metas, modelos de e-mail e envio pela conta Google de cada vendedor.
Em agosto o produto mudou de direção. Virou uma plataforma multiempresa em que cada negócio, como um restaurante ou uma barbearia, recebe um painel com os módulos do seu setor.
02
Ponto de partida
A primeira versão atendia um único time de vendas, rodando com dados de demonstração.
3
papéis: vendedor, gestor e administrador
600
contas simuladas no modo demonstração
1
empresa por instalação
03
Linha do tempo
56 commits no histórico do projeto. Estes são os marcos.
02 de jul. de 2026
Primeira versão
CRM de vendas com um portal por papel e modo demonstração com dados simulados.
09 de jul. de 2026
Integração com Gmail
Conexão OAuth com a conta Google, tokens cifrados, limite de envio e um botão de desligar a integração inteira.
10 de jul. de 2026
Login com Google
Identidade individual por conta Google, além do login por senha.
08 de ago. de 2026
Virada para multiempresa
Fundação refeita: empresa e setor dentro da sessão assinada, cadastro de empresa com seu dono e o primeiro módulo completo, Clientes, no padrão repositório, serviço e rota.
09 de ago. de 2026
Módulos de operação
Caixa, catálogo, atendimentos, estoque e previsão de faturamento.
11 de ago. de 2026
CRM e análises
Segmentos de clientes com consentimento da LGPD e painéis de análise por setor.
15 de ago. de 2026
Banco de dados real
Saída do modo demonstração para o Postgres, com os dados do esquema antigo guardados em backup antes da troca.
08 de set. de 2026
Acesso restrito ao alfa
Login fechado para o grupo do teste alfa.
04
Mapa de decisões
Reconstruído a partir do histórico de commits, do README e da documentação do projeto.
01
Refazer a fundação, não o produto inteiro
Autenticação, o padrão de repositórios e o design system continuaram. Saiu o domínio de vendas e entrou um modelo genérico de negócio.
02
A empresa vem da sessão
Toda rota de negócio lê a empresa de cabeçalhos que o middleware injeta a partir da sessão assinada. O navegador não tem como pedir os dados de outra empresa.
03
Setor é configuração
Um registro em código define quais módulos cada setor ativa e como cada coisa se chama. Um setor novo é uma entrada nova, não uma tabela nova.
04
Um modelo de dados para todos
Clientes, itens de catálogo, atendimentos, lançamentos e estoque servem a todos os setores, com um campo flexível para o que é específico de cada um.
05
Demonstração sem infraestrutura
Uma fábrica de repositórios alterna entre dados em memória e Prisma com Postgres. Dá para avaliar o produto inteiro sem configurar banco nenhum.
06
Alfa com escopo mínimo
O teste alfa atende só donos de negócio, um por empresa, sem gestão de equipe. Menos rotas no ar significa menos superfície de ataque.
05
Arquitetura
As mesmas cinco camadas desta página, aplicadas ao projeto.
000 m/interface
- Painel que se molda ao setor
- Design system com shadcn/ui
- Gráficos com Recharts
030 m/rota
- Middleware com controle por papel
- Sessão assinada com HMAC-SHA256
- Cabeçalhos forjados pelo cliente descartados
060 m/serviço
- Serviços por módulo
- Validação com Zod
- Registro de setores e módulos
090 m/repositório
- Fábrica de repositórios: memória ou Prisma
- Toda consulta filtrada por empresa
- Exclusão lógica
120 m/persistência
- Postgres no Supabase
- Senhas com scrypt
- Tokens OAuth cifrados com AES-256-GCM
06
Código
Um trecho do repositório privado, só para mostrar a camada funcionando.
A empresa nunca vem do navegador
Todo módulo de negócio começa por aqui. O identificador da empresa sai da sessão assinada, e sem ele a rota nem continua.
// Lê identidade + tenant dos headers injetados pelo middleware (assinados
// via cookie HMAC — o client não forja). Use em route handlers.
export async function getApiContext(): Promise<ApiContext | null> {
const h = await headers();
const userId = h.get("x-user-id");
const role = h.get("x-user-role") as Role | null;
if (!userId || !role) return null;
return { userId, role, companyId: h.get("x-company-id") };
}
/**
* Contexto que EXIGE empresa (módulos de negócio). SK_ADMIN sem empresa
* selecionada recebe 400 — a visão cross-tenant tem rotas próprias (/api/sk).
*/
export async function requireCompany(): Promise<RequireCompanyResult> {
const ctx = await getApiContext();
if (!ctx) {
return { error: NextResponse.json({ error: "Não autorizado" }, { status: 401 }) };
}
if (!ctx.companyId) {
return {
error: NextResponse.json(
{ error: "Nenhuma empresa no contexto." },
{ status: 400 }
),
};
}
return { ctx: { ...ctx, companyId: ctx.companyId } };
}O mesmo item, nomes diferentes por setor
Uma tabela só no banco, com a linguagem de cada negócio na tela. É o que permite atender setores novos sem mexer no modelo de dados.
// Terminologia do catálogo por setor (o mesmo CatalogItem se chama "prato"
// num restaurante e "serviço" num salão).
export function catalogTerms(sector: Sector | null): CatalogTerms {
switch (sector) {
case "CABELEIREIRO":
return { title: "Serviços", singular: "serviço", plural: "serviços", addLabel: "Novo serviço" };
case "RESTAURANTE":
default:
return { title: "Cardápio", singular: "item", plural: "itens", addLabel: "Novo item" };
}
}07
Resultado
2
setores ativos: restaurante e barbearia
11
tabelas num modelo comum a todos os setores
3
papéis: equipe da plataforma, dono e funcionário
Estado atual
Em teste alfa com donos de negócio, começando pela barbearia.