Um template completo e production-ready para aplicações SaaS, construído com as tecnologias mais modernas do ecossistema JavaScript/TypeScript.
Este projeto serve como base para qualquer aplicação SaaS que precise de:
- Autenticação completa com gerenciamento de sessão e JWT
- Pagamentos e assinaturas integrados ao Stripe
- Banco de dados com ORM tipado e migrations
- Proteção de rotas no middleware e nas APIs
- UI moderna com componentes reutilizáveis e design system consistente
A ideia é eliminar as semanas de boilerplate que todo projeto SaaS exige, entregando uma base sólida, bem estruturada e pronta para escalar.
| Camada | Tecnologia |
|---|---|
| Framework | Next.js 16 (App Router) |
| Runtime | Bun |
| Linguagem | TypeScript |
| Autenticação | Better Auth |
| Pagamentos | Stripe via @better-auth/stripe |
| Banco de dados | PostgreSQL (Supabase) |
| ORM | Drizzle ORM |
| Validação | Zod |
| Formulários | React Hook Form |
| UI Components | shadcn/ui + Tailwind CSS v4 |
| Notificações | Sonner |
| JWT | jose |
- Cadastro e login com e-mail e senha
- Gerenciamento de sessão via cookies (Better Auth)
- JWT com payload customizado (plano e status da subscription)
- Refresh automático de sessão
- Middleware de proteção de rotas públicas e privadas
- Criação automática de customer no Stripe no cadastro
- Página de pricing com planos Basic e Pro
- Checkout via
authClient.subscription.upgrade - Webhook handler para atualização automática do status da subscription
- Status da subscription embutido no JWT para verificação sem roundtrip ao banco
- Classe
AuthGuardpara proteger rotas de API com JWT - Verificação de subscription ativa nas rotas protegidas
- Padrão reutilizável e extensível para qualquer rota
- Componente
Surfacecomo camada base neutra reutilizável - Componente
Cardcom efeito de profundidade (layers sobrepostos) ChatInputcom design layered pronto para interfaces de AI- Dashboard com orb animado
- Formulários com validação em tempo real e feedback visual
src/
├── app/
│ ├── (private)/ # Rotas autenticadas
│ │ └── dashboard/ # Dashboard principal
│ ├── (public)/ # Rotas públicas
│ │ ├── pricing/ # Página de planos e assinatura
│ │ ├── sign-in/ # Login
│ │ └── sign-up/ # Cadastro
│ └── api/
│ ├── auth/[...all]/ # Handler do Better Auth (catch-all)
│ └── protected/ # Exemplo de rota protegida por JWT
├── components/
│ ├── ui/ # Design system (Button, Card, Input, Surface...)
│ ├── chat-input.tsx # Componente de input para AI
│ ├── login-form.tsx # Formulário de login com validação
│ └── signup-form.tsx # Formulário de cadastro com validação
├── db/
│ ├── index.ts # Instância do Drizzle ORM
│ └── schema.ts # Schema completo (auth + stripe + jwt)
├── lib/
│ ├── auth.ts # Configuração do Better Auth (servidor)
│ ├── auth-client.ts # Configuração do Better Auth (cliente)
│ ├── verifySession.ts # AuthGuard class para proteção de APIs
│ └── validations/
│ └── auth.ts # Schemas Zod de validação
└── middleware.ts # Middleware Next.js para proteção de rotas
git clone git@github.com:joaodadas/SaaS-Template.git
cd SaaS-Template
bun installCrie um arquivo .env na raiz:
# Better Auth
BETTER_AUTH_SECRET=seu_secret_aqui
BETTER_AUTH_URL=http://localhost:3000
# Banco de dados (Supabase ou outro PostgreSQL)
DATABASE_URL=postgresql://usuario:senha@host:5432/banco
# Stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PRICE_ID_BASIC=price_...
STRIPE_PRICE_ID_PRO=price_...bunx drizzle-kit generate
bunx drizzle-kit migrateNo dashboard do Stripe, crie um endpoint apontando para:
https://seu-dominio.com/api/auth/stripe/webhook
Eventos necessários:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcheckout.session.completed
Para desenvolvimento local, use o Stripe CLI:
stripe listen --forward-to localhost:3000/api/auth/stripe/webhookbun devCadastro (/sign-up)
↓
Stripe Customer criado automaticamente
↓
Redirecionado para /pricing
↓
Seleciona plano → Stripe Checkout
↓
Webhook atualiza subscription no banco
↓
Redirecionado para /dashboard
↓
Login futuro (/sign-in)
↓
JWT gerado com plano e status da subscription
↓
/dashboard (se ativo) ou /pricing (se sem plano)
Em src/lib/auth.ts, adicione o plano ao array plans dentro da configuração do plugin stripe:
plans: [
{ name: 'basic', priceId: process.env.STRIPE_PRICE_ID_BASIC! },
{ name: 'pro', priceId: process.env.STRIPE_PRICE_ID_PRO! },
{ name: 'enterprise', priceId: process.env.STRIPE_PRICE_ID_ENTERPRISE! }, // novo
]import { AuthGuard } from '@/lib/verifySession';
export async function GET(request: Request) {
const guard = new AuthGuard(request);
const auth = await guard.verifySubscription();
if (!auth.authorized) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
return Response.json({ data: 'conteúdo protegido' });
}MIT