Criar um boilerplate SaaS com Claude Code: Next.js, auth, billing, tenants e testes
Use Claude Code para criar um starter SaaS pago com Next.js, auth, billing, tenants, auditoria, testes e checklist.
Um boilerplate SaaS é a base reutilizável de um produto web pago: autenticação, cobrança, tenants, papéis, e-mail, dashboard, área admin, logs de auditoria, variáveis de ambiente, testes, documentação e checklist de deploy. Claude Code consegue gerar essa base rapidamente, mas um starter pronto para vender precisa de limites mais fortes do que uma demo.
O atalho perigoso é tratar “a aplicação roda” como conclusão. Um tenant é uma empresa, workspace ou conta de cliente dentro da mesma aplicação. Se a verificação de tenant for fraca, um cliente pode ver dados de outro. RBAC significa controle de acesso baseado em papéis: owner, admin, billing, member e viewer não devem ter os mesmos poderes. Um audit log registra quem fez o quê e quando. Sem isso, suporte, incidentes e vendas enterprise ficam mais difíceis.
Este guia mostra como orientar Claude Code e estruturar um starter com Next.js App Router, TypeScript, Prisma, Stripe e Resend que pode virar produto pago ou template comercial. Use sempre fontes oficiais: Claude Code docs, Next.js docs, Auth.js, Prisma schema docs, Stripe webhooks, Resend docs e OWASP Authentication Cheat Sheet.
Para aprofundar no ClaudeCodeLab, veja também autenticação segura, RBAC, validação com Zod e desenvolvimento de APIs com Claude Code.
Projetar a partir do produto pago
A primeira pergunta não é “quais componentes Claude Code deve gerar”. A primeira pergunta é “o que a pessoa que comprar este starter poderá lançar”. Um template com muitas telas, mas limites fracos entre tenants, vale menos do que um template menor com testes, documentação e critérios de revisão.
| Caso de uso | Base necessária | Caminho de monetização |
|---|---|---|
| Micro-SaaS solo | Login OAuth, plano individual, Stripe Checkout, dashboard de uso | Plano mensal simples |
| Ferramenta B2B | Tenants, convites, papéis, responsável de billing, auditoria | Preço por usuário ou workspace |
| Portal de conteúdo ou templates | Papel de comprador, histórico de downloads, e-mails, admin | Packs pagos e cursos |
| Ferramenta interna de IA | SSO, aprovações, política por IP ou domínio, logs | Consultoria de implementação |
Não diga que um boilerplate elimina revisão jurídica, fiscal, de privacidade ou segurança. Impostos no Stripe, reembolsos, termos de uso, política de privacidade, retenção de dados, suporte e revisão de permissões continuam exigindo avaliação humana. Claude Code acelera a implementação, mas não transfere a responsabilidade do produto.
flowchart LR
A["Marketing site"] --> B["Auth"]
B --> C["Tenant and roles"]
C --> D["Dashboard"]
C --> E["Billing"]
C --> F["Admin"]
D --> G["Audit logs"]
E --> G
F --> G
G --> H["Tests and release checklist"]
Dar a Claude Code um contrato CLAUDE.md
Se você pedir apenas “crie uma aplicação SaaS”, Claude Code pode misturar UI, rotas API, modelos e regras de negócio em um diff grande. CLAUDE.md transforma a tarefa em contrato. Em linguagem simples, é o harness: a estrutura de trabalho que mantém as mudanças do agente revisáveis.
# CLAUDE.md
## Product goal
Build a paid SaaS starter that can be reused for real products.
Do not claim the starter removes legal, tax, privacy, or security review.
## Stack
- Next.js App Router with TypeScript
- Prisma and PostgreSQL
- Auth.js for OAuth/session integration
- Stripe Checkout and billing webhooks
- Resend for transactional email
- Vitest and Playwright for acceptance tests
## Required boundaries
- Every business record belongs to a tenantId.
- Never trust tenantId from the browser without checking membership.
- Roles are OWNER, ADMIN, BILLING, MEMBER, VIEWER.
- Billing routes require OWNER or BILLING.
- Admin routes require OWNER or ADMIN.
- State-changing routes write an audit log.
- Secrets must be read through src/lib/env.ts and never hardcoded.
- Include tests for forbidden tenant access and webhook idempotency.
## Review output
After edits, list changed files, commands run, risks, and manual checks.
Essas regras bloqueiam atalhos comuns: confiar em tenantId vindo do navegador, deixar segredos no código, permitir billing para um member ou mudar estado sem auditoria.
Organizar por responsabilidade
Uma demo pequena pode manter tudo em uma pasta de rota. Um SaaS não. Auth, billing, equipe, e-mail e auditoria se cruzam rápido, então a estrutura precisa mostrar os limites.
src/
app/
(marketing)/
(auth)/
(dashboard)/dashboard/page.tsx
(admin)/admin/page.tsx
api/
billing/checkout/route.ts
billing/webhook/route.ts
tenants/invite/route.ts
components/
dashboard/
pricing/
ui/
lib/
auth.ts
env.ts
prisma.ts
tenant.ts
audit.ts
email.ts
stripe.ts
tests/
acceptance/saas.spec.ts
prisma/
schema.prisma
Isso também ajuda nos prompts. Você pode pedir “altere apenas o webhook de billing e os testes relacionados” sem deixar o agente tocar a aplicação inteira.
Usar um schema Prisma com tenant no centro
O schema abaixo é um mínimo prático. Tenant representa empresa, workspace ou conta de cliente. Membership conecta usuário, tenant e papel. Subscription guarda o estado do Stripe. AuditLog registra mudanças importantes.
// prisma/schema.prisma
enum Role {
OWNER
ADMIN
BILLING
MEMBER
VIEWER
}
enum Plan {
FREE
STARTER
PRO
}
enum SubscriptionStatus {
TRIALING
ACTIVE
PAST_DUE
CANCELED
}
model User {
id String @id @default(cuid())
email String @unique
name String?
memberships Membership[]
auditLogs AuditLog[]
createdAt DateTime @default(now())
}
model Tenant {
id String @id @default(cuid())
name String
slug String @unique
plan Plan @default(FREE)
memberships Membership[]
subscription Subscription?
auditLogs AuditLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Membership {
id String @id @default(cuid())
userId String
tenantId String
role Role @default(MEMBER)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, tenantId])
@@index([tenantId, role])
}
model Subscription {
id String @id @default(cuid())
tenantId String @unique
stripeCustomerId String @unique
stripeSubscriptionId String? @unique
status SubscriptionStatus @default(TRIALING)
priceId String?
currentPeriodEnd DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
updatedAt DateTime @updatedAt
}
model AuditLog {
id String @id @default(cuid())
tenantId String
actorId String?
action String
metadata Json?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
actor User? @relation(fields: [actorId], references: [id])
createdAt DateTime @default(now())
@@index([tenantId, createdAt])
}
Produtos reais normalmente adicionam faturas, endereços, contadores de uso, API keys, retenção de dados e notas de suporte. Não é preciso modelar tudo no primeiro dia. O essencial é tornar a fronteira do tenant impossível de ignorar.
Validar variáveis de ambiente
Segredos não devem ficar em código, prints ou commits. .env.example deve listar apenas nomes; os valores reais ficam em Vercel, Cloudflare, AWS, GitHub Actions ou outra plataforma. env.ts falha cedo quando uma chave importante está faltando.
// src/lib/env.ts
import { z } from "zod";
export const env = z
.object({
DATABASE_URL: z.string().url(),
AUTH_SECRET: z.string().min(32),
NEXT_PUBLIC_APP_URL: z.string().url(),
STRIPE_SECRET_KEY: z.string().startsWith("sk_"),
STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_"),
STRIPE_PRICE_STARTER: z.string().min(1),
RESEND_API_KEY: z.string().startsWith("re_"),
EMAIL_FROM: z.string().email(),
})
.parse(process.env);
Verificar tenant e papel no servidor
O navegador pode enviar tenantId, mas o servidor não deve confiar nele. Antes de ler ou modificar dados de negócio, carregue a membership no servidor.
// src/lib/tenant.ts
import { Role } from "@prisma/client";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
const roleRank: Record<Role, number> = {
VIEWER: 1,
MEMBER: 2,
BILLING: 3,
ADMIN: 4,
OWNER: 5,
};
export async function requireTenant(tenantId: string, minimumRole: Role = "MEMBER") {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const membership = await prisma.membership.findUnique({
where: {
userId_tenantId: {
userId: session.user.id,
tenantId,
},
},
include: { tenant: true },
});
if (!membership || roleRank[membership.role] < roleRank[minimumRole]) {
throw new Error("Forbidden tenant access");
}
return {
userId: session.user.id,
tenant: membership.tenant,
role: membership.role,
};
}
Um erro comum é colocar tenantId em um hidden input e usar direto no update. Hidden input não é segredo. Toda escrita deve verificar a membership no servidor.
Tratar Stripe Webhooks como eventos repetíveis
Webhook é uma notificação externa. Pode chegar mais de uma vez ou atrasado. Um starter sério verifica assinatura, exige metadata.tenantId, usa upsert e grava audit log.
// src/app/api/billing/webhook/route.ts
import Stripe from "stripe";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { SubscriptionStatus } from "@prisma/client";
import { env } from "@/lib/env";
import { prisma } from "@/lib/prisma";
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
function toStatus(status: Stripe.Subscription.Status): SubscriptionStatus {
if (status === "active") return "ACTIVE";
if (status === "past_due") return "PAST_DUE";
if (status === "canceled") return "CANCELED";
return "TRIALING";
}
export async function POST(request: Request) {
const body = await request.text();
const signature = (await headers()).get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "Missing Stripe signature" }, { status: 400 });
}
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, env.STRIPE_WEBHOOK_SECRET);
} catch {
return NextResponse.json({ error: "Invalid Stripe signature" }, { status: 400 });
}
if (
event.type === "customer.subscription.created" ||
event.type === "customer.subscription.updated" ||
event.type === "customer.subscription.deleted"
) {
const subscription = event.data.object as Stripe.Subscription;
const tenantId = subscription.metadata.tenantId;
if (!tenantId || typeof subscription.customer !== "string") {
return NextResponse.json({ error: "Missing tenant metadata" }, { status: 400 });
}
await prisma.subscription.upsert({
where: { tenantId },
create: {
tenantId,
stripeCustomerId: subscription.customer,
stripeSubscriptionId: subscription.id,
status: toStatus(subscription.status),
priceId: subscription.items.data[0]?.price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
update: {
stripeSubscriptionId: subscription.id,
status: toStatus(subscription.status),
priceId: subscription.items.data[0]?.price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
await prisma.auditLog.create({
data: {
tenantId,
action: `stripe.${event.type}`,
metadata: { eventId: event.id, subscriptionId: subscription.id },
},
});
}
return NextResponse.json({ received: true });
}
Centralizar e-mail e auditoria
Convites, falhas de pagamento, reset de senha e alertas admin usam e-mail. Mudanças de billing, papéis e configurações de tenant usam audit logs. Helpers compartilhados tornam o produto mais fácil de revisar.
// src/lib/email.ts
import { Resend } from "resend";
import { env } from "@/lib/env";
const resend = new Resend(env.RESEND_API_KEY);
export async function sendTenantInviteEmail(input: {
to: string;
tenantName: string;
inviteUrl: string;
}) {
return resend.emails.send({
from: env.EMAIL_FROM,
to: input.to,
subject: `${input.tenantName} invited you`,
html: `<p>You were invited to ${input.tenantName}.</p><p><a href="${input.inviteUrl}">Accept invite</a></p>`,
});
}
// src/lib/audit.ts
import { prisma } from "@/lib/prisma";
export async function writeAuditLog(input: {
tenantId: string;
actorId?: string;
action: string;
metadata?: Record<string, unknown>;
}) {
return prisma.auditLog.create({
data: {
tenantId: input.tenantId,
actorId: input.actorId,
action: input.action,
metadata: input.metadata,
},
});
}
Definir acceptance tests antes de polir a UI
Acceptance test verifica se a funcionalidade é aceitável do ponto de vista do usuário. Em um SaaS pago, os caminhos de recusa costumam ser tão importantes quanto o caminho feliz.
// tests/acceptance/saas.spec.ts
import { test, expect } from "@playwright/test";
test("member cannot open billing settings", async ({ page }) => {
await page.goto("/test-login?role=MEMBER");
await page.goto("/dashboard/acme/billing");
await expect(page.getByText("Forbidden")).toBeVisible();
});
test("billing user can open billing settings", async ({ page }) => {
await page.goto("/test-login?role=BILLING");
await page.goto("/dashboard/acme/billing");
await expect(page.getByRole("heading", { name: "Billing" })).toBeVisible();
});
test("tenant switch does not leak another tenant data", async ({ page }) => {
await page.goto("/test-login?tenant=acme");
await page.goto("/dashboard/other-team/settings");
await expect(page.getByText("Forbidden")).toBeVisible();
});
O erro comum é testar apenas login, criação de workspace, pagamento e dashboard. Teste também bloqueio de outro tenant, member sem acesso a billing e webhook repetido sem corromper a assinatura.
Checklist de lançamento
Antes de publicar ou vender o starter, revise pelo menos estes pontos.
- Metadata, hero image, links oficiais, links internos e CTA estão presentes
.env.examplecontém nomes, não segredos reais- Toda atualização de negócio verifica membership do tenant no servidor
- OWNER, ADMIN, BILLING, MEMBER e VIEWER têm testes distintos
- Stripe Webhook tem verificação de assinatura e idempotência
- Falhas de e-mail têm retry ou reenvio manual
- Admin e audit logs funcionam em produção
- Termos, privacidade, reembolso, impostos e retenção foram revisados por humanos
- README documenta setup local, seed, testes e deploy
Empacotar como produto
Para vender um SaaS boilerplate, código é só parte do valor. Compradores precisam de docs de setup, mapa de variáveis de ambiente, configuração do Stripe, seed data, acceptance tests e limites claros de suporte.
| Pacote | Conteúdo | Melhor para |
|---|---|---|
| Free checklist | Exemplo CLAUDE.md, mapa env, checklist de release | Builders validando a ideia |
| Starter template | Next.js, Prisma, Auth.js, Stripe, Resend, testes | MVP em um fim de semana |
| Pro template | Admin, audit logs, usage billing, convites, docs | Lançar SaaS pago com seriedade |
| Team rollout | Revisão de repo, treinamento Claude Code, regras de review | Empresas adotando Claude Code |
ClaudeCodeLab oferece uma cola gratuita, produtos e templates Claude Code e treinamento ou consultoria de implementação. Use o material gratuito para validar o fluxo e passe para templates ou consultoria quando o starter precisar encaixar em um repositório real.
Resultado na prática
Em um repositório pequeno de validação, Masa começou com um prompt amplo demais: “crie o dashboard”. A tela funcionou, mas a navegação direta para outro tenant não foi bloqueada e o endpoint Stripe Webhook não verificava assinatura. Depois de colocar limites de tenant, papéis, audit logs e acceptance tests em CLAUDE.md, Claude Code gerou diffs menores e a revisão ficou objetiva. A lição é prática: o valor de um SaaS boilerplate está nos limites, testes e documentação operacional, não na lista mais longa de funcionalidades.
PDF grátis: cheatsheet do Claude Code
Informe seu e-mail e baixe uma página com comandos, hábitos de revisão e workflows seguros.
Cuidamos dos seus dados e não enviamos spam.
Sobre o autor
Masa
Engenheiro focado em workflows práticos com Claude Code.
Artigos relacionados
Workflow Obsidian para CLAUDE.md com Claude Code
Transforme notas de trabalho do Obsidian em notas operacionais CLAUDE.md para preservar contexto.
Claude Code Revenue CTA Routing: artigos para PDF, Gumroad e consultoria
Um fluxo com Claude Code para levar leitores ao PDF grátis, Gumroad ou consultoria conforme intenção.
Regras de handoff para equipes com Claude Code: evidências, permissões, rollback e receita
Formato prático para entregar trabalho do Claude Code com prova, permissões, rollback, PDF grátis, Gumroad e consultoria.