Guia de setup
Do zero ao deploy. Configure Supabase, Stripe e Resend, entenda o multi-tenancy e publique na Vercel.
Visão geral#
O SaaS Starter Kit é uma base reutilizável organizada em um monorepo com duas aplicações web:
- apps/dashboard
porta 3000App dos assinantes e dos usuários que eles convidam. Perfil, projetos, membros e assinatura/billing.
- apps/admin
porta 3001App exclusiva do dono do SaaS (super-admin). Métricas e gestão da plataforma.
profiles.is_super_admin). Isso é diferente do papel admin de um projeto (em project_members), que vive no Dashboard. Nunca misture os dois.Stack
Turborepo (monorepo) · pnpm · Next.js (App Router) · React · TypeScript · Tailwind CSS · Supabase (Postgres + Auth + RLS) · Stripe (assinaturas) · Resend (e-mail) · next-intl (pt-BR/en/es) · Vercel (deploy).
Estrutura
.
├── apps/
│ ├── dashboard/ # app do assinante (3000)
│ └── admin/ # app do dono do SaaS (3001)
├── packages/ # código compartilhado futuro
├── supabase/migrations/ # migrations SQL versionadas
├── docs/ # documentação por módulo
├── .env.example # checklist de variáveis
└── turbo.jsonRequisitos#
Antes de começar, você vai precisar de:
- Node.js LTS
- pnpm
- Git
- Conta no GitHub
- Conta no Supabase
- Conta no Stripe
- Conta no Resend
- Conta na Vercel
Instalação#
Clone o repositório e instale as dependências na raiz:
git clone https://github.com/PedroFelipe-git/projeto-SaaS
cd projeto-SaaS
pnpm installComandos úteis do dia a dia:
pnpm dev # roda as duas apps (turbo)
pnpm --filter dashboard dev # só a dashboard (3000)
pnpm --filter admin dev # só a admin (3001)
pnpm build # build de tudoConfiguração do Supabase#
Supabase cuida do banco (Postgres), autenticação e RLS.
- 1. Crie um projeto em supabase.com/dashboard (guarde a senha do banco).
- 2. Em
Project Settings → API, copie oProject URL, a chaveanone a chaveservice_role. - 3. Preencha em
apps/dashboard/.env.local(e o mesmo emapps/admin/.env.local):
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...- 4. Aplique as migrations de
supabase/migrations/em ordem crescente, noSQL Editor. - 5.
Authentication → Providers → Email: desligue "Confirm email" em desenvolvimento (reative em produção). - 6.
Authentication → URL Configuration → Redirect URLs: adicionehttp://localhost:3000/**.
Super-admin (app Admin)
Para liberar o acesso à app de admin, rode no SQL Editor:
update public.profiles set is_super_admin = true
where user_id = (
select id from auth.users where email = 'SEU_EMAIL'
);requireSuperAdmin(). A chave service_role nunca vai ao navegador — use apenas em webhooks e no servidor.Configuração do Stripe#
Assinaturas mensal/anual via Stripe Checkout, com Customer Portal e ativação por webhook.
- 1. Em dashboard.stripe.com (modo de teste),
Developers → API keys: copiepk_test_...esk_test_.... - 2. Crie um produto com preços mensal e anual em
Product catalog → Add product. Copie oprod_...e os doisprice_.... - 3. Em
supabase/migrations/0007_seed_pro_plan.sql, troque os IDs pelos seus e rode no SQL Editor. - 4. Preencha em
apps/dashboard/.env.local:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...Webhook local (Stripe CLI)
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripeCopie o whsec_... para STRIPE_WEBHOOK_SECRET e reinicie o dev server.
checkout.session.completed e customer.subscription.updated/deleted gravam a tabela subscriptions via service_role. Valores monetários ficam em centavos.Resend (e-mail)
Convites de membros são enviados via Resend. Em resend.com → API Keys, crie a chave re_... e preencha:
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=onboarding@resend.dev
NEXT_PUBLIC_APP_URL=http://localhost:3000Multi-tenancy#
Cada assinante organiza o trabalho em projetos. Um projeto tem membros com papéis owner, admin e member. O isolamento é garantido por RLS no Postgres.
Projeto ativo
O projeto ativo fica no cookie active_project. A página resolve: cookie válido → senão o primeiro projeto. Trocar chama setActiveProject (grava o cookie) e router.refresh().
RLS sem recursão
O select de projetos retorna só aqueles em que você é membro. Para evitar recursão nas políticas, o acesso usa funções security definer: is_project_member(id) e is_project_admin(id).
- • Criar projeto:
owner_user_id = auth.uid(); um trigger te adiciona comoowner. - • Renomear: owner/admin. Excluir: só o dono.
- •
createProjectchecamax_projectsdo plano antes de inserir.
Convites
Owner/admin convida por e-mail → cria uma invitation (token + expiração) → Resend envia o link /convite/<token>. O convidado faz login e aceita; acceptInvitation roda via service_role (pois a RLS ainda bloquearia o convidado) e insere o project_member.
Deploy (Vercel)#
As duas apps são dois projetos na Vercel apontando para o mesmo repositório, cada um com seu Root Directory.
- •
apps/dashboard→ um projeto Vercel (Root Directory =apps/dashboard). - •
apps/admin→ outro projeto Vercel (Root Directory =apps/admin).
Preset detectado: Next.js. O packageManager fixado garante o pnpm certo. Cada git push para main republica automaticamente os dois projetos.
Variáveis de ambiente
Copie do .env.example em cada projeto Vercel:
- • dashboard: Supabase (3), Stripe (3), Resend (3) e
NEXT_PUBLIC_APP_URL. - • admin: Supabase (3).
Checklist de produção
- Reativar “Confirm email” no Supabase.
- Adicionar as URLs de produção em Supabase → Auth → Redirect URLs.
- Verificar um domínio no Resend e ajustar RESEND_FROM_EMAIL.
- Criar o webhook de produção no Stripe (endpoint https://<dashboard>/api/webhooks/stripe) e usar o whsec_ de produção.
- Trocar as chaves do Stripe de teste por produção ao cobrar de verdade.
- NEXT_PUBLIC_APP_URL apontando para a URL pública.
FAQ / Troubleshooting#
A assinatura não ativa depois do pagamento
A ativação depende do webhook, não do redirect de sucesso. Em dev, confirme que o stripe listen está rodando e que STRIPE_WEBHOOK_SECRET corresponde ao whsec_ exibido pela CLI. Reinicie o dev server após alterar o .env.local.
Erro de recursão infinita na RLS
Não consulte project_members diretamente dentro de uma política dessa mesma tabela. Use as funções security definer is_project_member / is_project_admin.
.insert().select() retorna vazio ou dá erro
O select encadeado exige que a linha nova seja legível pela RLS. Se o acesso depende de um trigger da mesma operação, faça o insert e depois um select separado.
O convidado não consegue aceitar o convite
A página de aceite lê o convite com o cliente admin (service_role), pois a RLS bloquearia o convidado que ainda não é membro. Confira também se o convite não expirou.
O e-mail de convite não chega
Em teste, sem domínio verificado, o Resend só envia para o e-mail dono da conta. Verifique um domínio para enviar a qualquer destinatário.
Migrations
Ficam em supabase/migrations/NNNN_*.sql, numeradas. Aplique em ordem crescente no SQL Editor. Nunca edite uma migration já aplicada — crie uma nova.
Pronto para configurar?
Clone o repositório e siga os passos acima.