CRM moderno com IA integrada para times de vendas
Important
Siga TODOS os passos na ordem. Configure o Supabase completamente antes de fazer deploy na Vercel!
📖 Instruções detalhadas (2 min)
- Acesse supabase.com e crie uma conta
- Clique em "New Project"
- Preencha:
Campo Valor Name meu-crm(ou outro nome)Database Password Senha forte (guarde!) Region South America (São Paulo) - Clique em "Create new project"
- ⏳ Aguarde ~2 minutos
📖 Instruções detalhadas (2 min)
-
No menu lateral, clique em SQL Editor
-
Clique em "New query"
-
Acesse e copie TODO o conteúdo:
-
Cole no SQL Editor do Supabase
-
Clique no botão "Run" (ou
Ctrl+Enter)
[!NOTE] A mensagem
"Success. No rows returned"é normal - significa que funcionou!
Caution
Este passo é OBRIGATÓRIO! Sem ele, o login não funciona corretamente.
📖 Instruções detalhadas (1 min)
O Auth Hook injeta o company_id no token JWT. É necessário para as permissões funcionarem.
- No menu lateral, vá em Authentication
- Clique na aba Hooks
- Encontre "Customize Access Token (JWT)"
- Clique no toggle para habilitar
- Configure:
Campo Valor Schema publicFunction custom_access_token_hook - Clique em Save
Você precisa criar 5 funções. Para cada uma:
- No menu lateral → Edge Functions
- Clique "Create a new function"
- Digite o nome exatamente como mostrado
- Cole o código
- Clique "Deploy"
📄 Função 1: setup-instance
Cria a primeira empresa e usuário admin.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type' } })
}
try {
const { companyName, email, password } = await req.json()
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const { data: isInitialized, error: initError } = await supabaseAdmin
.rpc('is_instance_initialized')
if (initError) throw initError
if (isInitialized) return new Response(JSON.stringify({ error: 'Instance already initialized' }), { status: 400, headers: { 'Content-Type': 'application/json' } })
const { data: company, error: companyError } = await supabaseAdmin
.from('companies')
.insert({ name: companyName })
.select()
.single()
if (companyError) throw companyError
const { data: user, error: userError } = await supabaseAdmin.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: {
role: 'admin',
company_id: company.id
}
})
if (userError) {
await supabaseAdmin.from('companies').delete().eq('id', company.id)
throw userError
}
// Aguardar trigger criar o profile
await new Promise(resolve => setTimeout(resolve, 500))
const { error: profileError } = await supabaseAdmin
.from('profiles')
.update({
company_id: company.id,
role: 'admin'
})
.eq('id', user.user.id)
if (profileError) {
await supabaseAdmin.auth.admin.deleteUser(user.user.id)
await supabaseAdmin.from('companies').delete().eq('id', company.id)
throw profileError
}
return new Response(
JSON.stringify({ message: 'Instance setup successfully', company, user }),
{ headers: { "Content-Type": "application/json", 'Access-Control-Allow-Origin': '*' } },
)
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { "Content-Type": "application/json", 'Access-Control-Allow-Origin': '*' } })
}
})📄 Função 2: create-user
Permite que admins criem novos usuários.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts"
import { createClient } from "https://esm.sh/@supabase/supabase-js@2"
serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type' } })
}
try {
const supabaseClient = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_ANON_KEY') ?? '',
{ global: { headers: { Authorization: req.headers.get('Authorization')! } } }
)
const { data: { user } } = await supabaseClient.auth.getUser()
if (!user) return new Response("Unauthorized", { status: 401 })
const { data: profile } = await supabaseClient
.from('profiles')
.select('role, company_id')
.eq('id', user.id)
.single()
if (profile?.role !== 'admin') {
return new Response("Forbidden: Only admins can create users", { status: 403 })
}
const { email, password, role } = await req.json()
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL') ?? '',
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? ''
)
const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({
email,
password,
email_confirm: true
})
if (createError) throw createError
const { error: profileError } = await supabaseAdmin
.from('profiles')
.insert({
id: newUser.user.id,
company_id: profile.company_id,
email: email,
role: role || 'vendedor'
})
if (profileError) {
await supabaseAdmin.auth.admin.deleteUser(newUser.user.id)
throw profileError
}
return new Response(
JSON.stringify({ message: 'User created successfully', user: newUser }),
{ headers: { "Content-Type": "application/json", 'Access-Control-Allow-Origin': '*' } },
)
} catch (error) {
return new Response(JSON.stringify({ error: error.message }), { status: 400, headers: { "Content-Type": "application/json", 'Access-Control-Allow-Origin': '*' } })
}
})📄 Função 3: list-users
Lista os usuários da empresa.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const authHeader = req.headers.get("authorization");
if (!authHeader) throw new Error("Missing authorization header");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const userClient = createClient(supabaseUrl, supabaseAnonKey, {
global: { headers: { Authorization: authHeader } },
});
const adminClient = createClient(supabaseUrl, supabaseServiceKey);
const { data: { user }, error: userError } = await userClient.auth.getUser();
if (userError || !user) throw new Error("Not authenticated");
const { data: profile, error: profileError } = await userClient
.from("profiles")
.select("role, company_id")
.eq("id", user.id)
.single();
if (profileError || !profile) throw new Error("Profile not found");
const usersWithStatus: any[] = [];
const { data: profiles } = await adminClient
.from("profiles")
.select("*")
.eq("company_id", profile.company_id)
.order("created_at", { ascending: false });
for (const p of profiles || []) {
usersWithStatus.push({
id: p.id,
email: p.email,
role: p.role,
company_id: p.company_id,
created_at: p.created_at,
status: 'active',
});
}
const { data: authData } = await adminClient.auth.admin.listUsers();
if (authData?.users) {
const profileIds = new Set((profiles || []).map(p => p.id));
for (const authUser of authData.users) {
if (profileIds.has(authUser.id)) continue;
const metadata = authUser.user_metadata || {};
if (metadata.company_id === profile.company_id) {
usersWithStatus.push({
id: authUser.id,
email: authUser.email || '',
role: metadata.role || 'vendedor',
company_id: metadata.company_id,
created_at: authUser.created_at,
status: 'pending',
invited_at: authUser.invited_at || authUser.created_at,
});
}
}
}
usersWithStatus.sort((a, b) =>
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
);
return new Response(
JSON.stringify({ success: true, users: usersWithStatus }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
);
} catch (error: any) {
return new Response(
JSON.stringify({ success: false, error: error.message }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
});📄 Função 4: delete-user
Permite que admins removam usuários.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const authHeader = req.headers.get("authorization");
if (!authHeader) throw new Error("Missing authorization header");
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseAnonKey = Deno.env.get("SUPABASE_ANON_KEY")!;
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const userClient = createClient(supabaseUrl, supabaseAnonKey, {
global: { headers: { Authorization: authHeader } },
});
const adminClient = createClient(supabaseUrl, supabaseServiceKey);
const { data: { user }, error: userError } = await userClient.auth.getUser();
if (userError || !user) throw new Error("Not authenticated");
const { data: profile, error: profileError } = await userClient
.from("profiles")
.select("role, company_id")
.eq("id", user.id)
.single();
if (profileError || !profile) throw new Error("Profile not found");
if (profile.role !== "admin") throw new Error("Only admins can delete users");
const { userId } = await req.json();
if (!userId) throw new Error("userId is required");
if (userId === user.id) throw new Error("Você não pode remover a si mesmo");
const { data: targetProfile } = await adminClient
.from("profiles")
.select("company_id")
.eq("id", userId)
.single();
if (targetProfile) {
await adminClient.from("profiles").delete().eq("id", userId);
}
const { error: deleteError } = await adminClient.auth.admin.deleteUser(userId);
if (deleteError) throw new Error(`Failed to delete user: ${deleteError.message}`);
return new Response(
JSON.stringify({ success: true, message: "User deleted successfully" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
);
} catch (error: any) {
return new Response(
JSON.stringify({ success: false, error: error.message }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
);
}
});📄 Função 5: accept-invite
Permite que usuários convidados aceitem o convite.
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
const adminClient = createClient(supabaseUrl, supabaseServiceKey);
const { email, password, token, name } = await req.json();
if (!email || !password || !token) {
throw new Error("Email, password and token are required");
}
const { data: invite, error: inviteError } = await adminClient
.from("company_invites")
.select("*")
.eq("token", token)
.single();
if (inviteError || !invite) {
return new Response(
JSON.stringify({ error: "Convite inválido ou não encontrado" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
if (invite.expires_at && new Date(invite.expires_at) < new Date()) {
return new Response(
JSON.stringify({ error: "Convite expirado" }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
if (invite.email && invite.email.toLowerCase() !== email.toLowerCase()) {
throw new Error("Este convite não é válido para este email");
}
const { data: authData, error: createError } = await adminClient.auth.admin.createUser({
email,
password,
email_confirm: true,
user_metadata: {
name: name || email.split("@")[0],
company_id: invite.company_id,
role: invite.role,
},
});
if (createError) throw createError;
const { error: profileError } = await adminClient
.from("profiles")
.insert({
id: authData.user.id,
email: email,
role: invite.role,
company_id: invite.company_id,
status: "active",
created_at: new Date().toISOString(),
});
if (profileError) {
await adminClient.auth.admin.deleteUser(authData.user.id);
throw profileError;
}
return new Response(
JSON.stringify({
user: authData.user,
message: "Convite aceito com sucesso!"
}),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 }
);
} catch (error: any) {
return new Response(
JSON.stringify({ error: error.message }),
{ headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 400 }
);
}
});Antes de continuar, verifique:
- ✔️ SQL executado com sucesso
- ✔️ Auth Hook configurado
- ✔️ 5 Edge Functions criadas e deployadas
📖 Instruções detalhadas (30 seg)
- No Supabase, clique em "Connect" (canto superior direito)
- Vá em "App Frameworks"
- Selecione: React → Vite → Supabase-js
- Na aba
.env, copie:
VITE_SUPABASE_URL=https://xxxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGci...💡 Cole num bloco de notas temporariamente.
📖 Instruções detalhadas (1 min)
- Clique na URL do projeto (
https://seu-projeto.vercel.app) - Complete o Setup Wizard:
- Nome da empresa
- Seu email e senha de admin
❌ "Apareceu tela de login mas não consigo criar conta"
Causa: Você pulou os passos do Supabase.
Solução: Volte e execute o SQL e crie as Edge Functions.
❌ "Erro ao fazer login / Página em branco"
Causa: Variáveis de ambiente incorretas.
Solução: Verifique na Vercel se as variáveis estão corretas (sem espaços extras).
❌ "Success. No rows returned" no SQL
Isso é NORMAL! ✅ Significa que o SQL executou corretamente.
❌ "Login funciona mas mostra 'Usuário' em vez de 'Admin'"
Causa: Auth Hook não configurado.
Solução: Volte ao Passo 3 e configure em Authentication → Hooks.
❌ "Function not found" ou erro ao criar conta
Causa: Edge Functions não criadas.
Solução: Volte ao Passo 4 e crie todas as 5 funções.
❌ "Error deploying function"
Causa: Código incompleto.
Solução: Verifique se copiou o código completo, incluindo os imports.
| Camada | Tecnologia |
|---|---|
| Frontend | React 19 • TypeScript • Vite • Tailwind CSS |
| Backend | Supabase (PostgreSQL • Auth • Edge Functions) |
| State | TanStack Query • Zustand |
| AI | Google Gemini (opcional) |
MIT License - Use como quiser! 🚀
Feito com ❤️ por Thales Laray | Escola de Automação