Documentação

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 3000

    App dos assinantes e dos usuários que eles convidam. Perfil, projetos, membros e assinatura/billing.

  • apps/admin porta 3001

    App exclusiva do dono do SaaS (super-admin). Métricas e gestão da plataforma.

Distinção crítica: a app Admin é só do dono (flag 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

text
.
├── 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.json

Requisitos#

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:

bash
git clone https://github.com/PedroFelipe-git/projeto-SaaS
cd projeto-SaaS
pnpm install

Comandos úteis do dia a dia:

bash
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 tudo

Configuração do Supabase#

Supabase cuida do banco (Postgres), autenticação e RLS.

  1. 1. Crie um projeto em supabase.com/dashboard (guarde a senha do banco).
  2. 2. Em Project Settings → API, copie o Project URL, a chave anon e a chave service_role.
  3. 3. Preencha em apps/dashboard/.env.local (e o mesmo em apps/admin/.env.local):
env
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
  1. 4. Aplique as migrations de supabase/migrations/ em ordem crescente, no SQL Editor.
  2. 5. Authentication → Providers → Email: desligue "Confirm email" em desenvolvimento (reative em produção).
  3. 6. Authentication → URL Configuration → Redirect URLs: adicione http://localhost:3000/**.

Super-admin (app Admin)

Para liberar o acesso à app de admin, rode no SQL Editor:

sql
update public.profiles set is_super_admin = true
where user_id = (
  select id from auth.users where email = 'SEU_EMAIL'
);
A app Admin só libera o acesso após o gate 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. 1. Em dashboard.stripe.com (modo de teste), Developers → API keys: copie pk_test_... e sk_test_....
  2. 2. Crie um produto com preços mensal e anual em Product catalog → Add product. Copie o prod_... e os dois price_....
  3. 3. Em supabase/migrations/0007_seed_pro_plan.sql, troque os IDs pelos seus e rode no SQL Editor.
  4. 4. Preencha em apps/dashboard/.env.local:
env
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_SECRET_KEY=sk_test_...

Webhook local (Stripe CLI)

bash
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe

Copie o whsec_... para STRIPE_WEBHOOK_SECRET e reinicie o dev server.

A ativação da assinatura vem do webhook, não do redirect. Os eventos 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:

env
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=onboarding@resend.dev
NEXT_PUBLIC_APP_URL=http://localhost:3000
Em teste, sem domínio verificado, o Resend só envia para o e-mail dono da conta.

Multi-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 como owner.
  • Renomear: owner/admin. Excluir: só o dono.
  • createProject checa max_projects do 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.

Abrir no GitHub