tRPC Setup Completo
Configure tRPC do zero: type-safe APIs, middleware de autenticação e client setup
Por que isso é importante
tRPC revoluciona desenvolvimento full-stack oferecendo type-safety completo entre client e server. Elimina bugs de integração, acelera desenvolvimento em 40% e garante APIs sempre sincronizadas com TypeScript.
📚 Aula Educacional
Esta aula apresenta os códigos de configuração do tRPC como exemplos educacionais. Você aprenderá a estrutura, conceitos e implementação sem modificar o projeto atual. Use estes códigos como referência para seus próprios projetos SaaS.
🎯 O que Você Vai Aprender
Server Setup:
- • Middleware de autenticação Clerk
- • Context com user session
- • Router structure profissional
- • Type-safe procedures
Client Setup:
- • React Query integration
- • Provider setup
- • Auto-typed hooks
- • Error handling
📦 Passo 1: Instalação de Dependências
⚠️ Requisitos Importantes
Certifique-se que Clerk Authentication já está configurado no projeto. tRPC usará o contexto de autenticação do Clerk para proteger procedures.
# Comandos para instalar tRPC
# Instalar dependências principais do tRPC
npm install @trpc/server @trpc/client @trpc/tanstack-react-query client-only server-only @tanstack/react-query superjson
# Verificar se Clerk já está instalado
npm list @clerk/nextjs🔧 Passo 2: Configurando Middleware
🎯 Por que Middleware First?
Configuramos o middleware primeiro para habilitar autenticação em toda a aplicação. Isso permite que tRPC acesse rotas protegidas e contexto de usuário.
// Configuração do middleware Clerk + tRPC
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
const isPublicRoute = createRouteMatcher([
'/sign-in(.*)',
'/sign-up(.*)',
'/',
'/api/inngest(.*)',
'/api/trpc(.*)', // 🎯 Importante: tRPC precisa ser público para auth flow
'/api/webhooks/stripe(.*)',
'/pricing(.*)',
]);
export default clerkMiddleware(async (auth, req) => {
if (!isPublicRoute(req)) {
await auth.protect();
}
});
export const config = {
matcher: [
// ✅ Skip Next.js internals e arquivos estáticos
'/((?!_next|[^?]*\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// ✅ Sempre executar para API routes
'/(api|trpc)(.*)',
],
};🏗️ Passo 3: tRPC Context e Initialization
✅ Arquitetura Profissional
Este setup cria context reutilizável, middleware de autenticação e procedures type-safe. Padrão usado em SaaS de milhões de usuários como Vercel e Railway.
// Context e inicialização do tRPC
import { initTRPC, TRPCError } from '@trpc/server';
import { cache } from 'react';
import superjson from 'superjson';
import { auth } from '@clerk/nextjs/server';
// 🎯 Context factory - chamado a cada request
export const createTRPCContext = cache(async () => {
/**
* @see: https://trpc.io/docs/server/context
*/
return { auth: await auth() };
});
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>;
// 🏗️ Inicializar tRPC com context tipado
const t = initTRPC.context<TRPCContext>().create({
/**
* @see https://trpc.io/docs/server/data-transformers
*/
transformer: superjson, // ✅ Serializa Date, Map, Set automaticamente
});
// 🔐 Middleware de autenticação reutilizável
const isAuthenticated = t.middleware(async ({ ctx, next }) => {
if (!ctx.auth.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' });
}
return next({
ctx: {
auth: ctx.auth, // ✅ Context tipado com user session
},
});
});
// 📡 Base router e procedure helpers
export const createTRPCRouter = t.router;
export const createCallerFactory = t.createCallerFactory;
export const baseProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthenticated);⚡ Passo 4: Query Client Configuration
// Configuração do React Query Client
import { defaultShouldDehydrateQuery, QueryClient } from '@tanstack/react-query';
import superjson from 'superjson';
export function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 30 * 1000, // 🚀 30s cache para reduzir requests
},
dehydrate: {
serializeData: superjson.serialize, // ✅ SSR + client sync
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrate: {
deserializeData: superjson.deserialize, // ✅ Hidratação type-safe
},
},
});
}🌐 Passo 5: App Router Structure
🔄 Estrutura Escalável
Router principal será expandido com modules específicos (messages, projects, users). Comentários mostram onde adicionar novos routers.
// Router principal do tRPC
import { createTRPCRouter } from '../init';
//import { messagesRouter } from '@/modules/messages/server/procedures';
//import { projectsRouter } from '@/modules/projects/server/procedures';
//import { usagesRouter } from '@/modules/usage/server/procedures';
import { subscriptionRouter } from '@/modules/subscription/server/procedures';
export const appRouter = createTRPCRouter({
//messages: messagesRouter,
//projects: projectsRouter,
//usages: usagesRouter,
subscription: subscriptionRouter,
});
// 🎯 Export type definition para client type-safety
export type AppRouter = typeof appRouter;🖥️ Passo 6: Server-Side Setup
// Server-side setup do tRPC
import 'server-only'; // 🔒 Garante que não vaze para client
import { createTRPCOptionsProxy } from '@trpc/tanstack-react-query';
import { cache } from 'react';
import { createTRPCContext } from './init';
import { makeQueryClient } from './query-client';
import { appRouter } from './routers/_app';
// 🎯 IMPORTANT: Stable getter para query client
// Retorna mesmo client durante o request
export const getQueryClient = cache(makeQueryClient);
export const trpc = createTRPCOptionsProxy({
ctx: createTRPCContext,
router: appRouter,
queryClient: getQueryClient,
});
// 📞 Direct caller para server components
export const caller = appRouter.createCaller(createTRPCContext);💻 Passo 7: Client-Side Setup
🎨 Client Provider Pattern
Provider separa server/client corretamente, evita hidration errors e garante performance otimizada com browser query client.
// Client-side setup do tRPC
'use client';
// ⬆️ Garante que Provider pode ser montado do server component
import superjson from 'superjson';
import type { QueryClient } from '@tanstack/react-query';
import { QueryClientProvider } from '@tanstack/react-query';
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import { createTRPCContext } from '@trpc/tanstack-react-query';
import { useState } from 'react';
import { makeQueryClient } from './query-client';
import type { AppRouter } from './routers/_app';
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
let browserQueryClient: QueryClient;
function getQueryClient() {
if (typeof window === 'undefined') {
// 🖥️ Server: sempre criar novo client
return makeQueryClient();
}
// 🌐 Browser: reutilizar client para evitar re-criação
// CRÍTICO para evitar suspense boundary issues
if (!browserQueryClient) browserQueryClient = makeQueryClient();
return browserQueryClient;
}
function getUrl() {
const base = (() => {
if (typeof window !== 'undefined') return '';
return process.env.NEXT_PUBLIC_APP_URL;
})();
return `${base}/api/trpc`;
}
export function TRPCReactProvider(
props: Readonly<{
children: React.ReactNode;
}>,
) {
const queryClient = getQueryClient();
const [trpcClient] = useState(() =>
createTRPCClient<AppRouter>({
links: [
httpBatchLink({
transformer: superjson,
url: getUrl(),
}),
],
}),
);
return (
<QueryClientProvider client={queryClient}>
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
{props.children}
</TRPCProvider>
</QueryClientProvider>
);
}🔗 Passo 8: Integração no Layout Principal
🎯 Finalizando a Integração
O último passo é envolver toda a aplicação com o TRPCReactProvider no layout principal. Isso disponibiliza os hooks tRPC em todos os componentes da aplicação.
import type { Metadata } from 'next';
import {
Geist,
Geist_Mono,
} from 'next/font/google';
import './globals.css';
import { TRPCReactProvider } from '@/trpc/client';
import { Toaster } from '@/components/ui/sonner';
import { ThemeProvider } from 'next-themes';
import { ClerkProvider } from '@clerk/nextjs';
const geistSans = Geist({
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: 'Create Next App',
description:
'Generated by create next app',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider
appearance={{
variables: {
colorPrimary: '#a48fff',
},
}}
>
<TRPCReactProvider>
<html
lang="pt-BR"
suppressHydrationWarning={
true
}
data-lt-installed="true"
>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
defaultTheme="light"
enableSystem
disableTransitionOnChange
>
<Toaster />
{children}
</ThemeProvider>
</body>
</html>
</TRPCReactProvider>
</ClerkProvider>
);
}🎨 Passo 9: Ajustando a Homepage
🎯 Landing Page Otimizada
Com tRPC configurado, agora temos uma homepage limpa e funcional que demonstra a integração das tecnologias. Esta é a estrutura base para expandir com dados dinâmicos.
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import Link from 'next/link';
export default function HomePage() {
return (
<div className="min-h-screen bg-gradient-to-br from-background-50 to-background-100">
<section className="container mx-auto px-4 py-20 text-center">
<h2 className="text-5xl font-bold mb-6 text-muted-foreground bg-clip-text">
O SaaS mais simples do mundo
</h2>
<p className="text-xl text-muted-foreground mb-8 max-w-2xl mx-auto">
Uma plataforma SaaS que te
ajuda a criar e gerenciar seus
projetos de forma simples e
eficiente.
</p>
<Link href="/pricing">
<Button>Comece agora</Button>
</Link>
</section>
<section className="container mx-auto px-4 py-16">
<div className="grid md:grid-cols-3 gap-8">
<Card>
<CardHeader>
<CardTitle>
Simples
</CardTitle>
<CardDescription>
Nossa plataforma é fácil
de usar
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-xs">
Sem complicações, sem
configurações
desnecessárias. Tudo que
você precisa em um
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
Seguro
</CardTitle>
<CardDescription>
Pagamentos via Stripe
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-xs">
Integração completa com
Stripe para pagamentos
seguros.
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>
Rápido
</CardTitle>
<CardDescription>
Setup em minutos
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-muted-foreground text-xs">
Comece a usar em poucos
cliques, sem
configurações complexas.
</p>
</CardContent>
</Card>
</div>
</section>
</div>
);
}📊 Passo 10: Router de Assinaturas
🎯 Procedures para SaaS
Exemplo prático de como criar procedures tRPC para gerenciar assinaturas. Este router demonstra consultas protegidas, validação Zod e tratamento de erros profissional.
import { createTRPCRouter, protectedProcedure } from '@/trpc/init';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { getSubscriptionByClerkId, cancelSubscription } from '@/lib/services/subscription';
export const subscriptionRouter = createTRPCRouter({
// Buscar subscription do usuário atual
getCurrent: protectedProcedure.query(async ({ ctx }) => {
try {
const subscription = await getSubscriptionByClerkId(ctx.auth.userId!);
return subscription;
} catch (error) {
console.error('Get current subscription failed:', error);
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Falha ao buscar assinatura atual',
});
}
}),
// Cancelar subscription
cancel: protectedProcedure
.input(
z.object({
stripeSubscriptionId: z.string().min(1, { message: 'Stripe Subscription ID is required' }),
}),
)
.mutation(async ({ input }) => {
try {
const subscription = await cancelSubscription(input.stripeSubscriptionId);
if (!subscription) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Assinatura não encontrada',
});
}
return subscription;
} catch (error) {
console.error('Cancel subscription failed:', error);
if (error instanceof TRPCError) {
throw error;
}
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Falha ao cancelar assinatura',
});
}
}),
});✅ Padrões Aplicados
protectedProcedure: Garante autenticação
Validação Zod: Input type-safe
Error Handling: TRPCError padronizado
Context Access: ctx.auth.userId
Query vs Mutation: Separação correta
Service Layer: Lógica isolada
🎨 Passo 11: Dashboard com tRPC
🎯 Integração Frontend Completa
Dashboard refatorado usando tRPC hooks para consumir as procedures de assinatura. Demonstra Suspense, loading states, mutations e invalidação de cache.
'use client';
import { Button } from '@/components/ui/button';
import { CheckCircle, AlertCircle } from 'lucide-react';
import { useSearchParams } from 'next/navigation';
import { Suspense, useState } from 'react';
import { useTRPC } from '@/trpc/client';
import { useSuspenseQuery, useMutation, useQueryClient } from '@tanstack/react-query';
// Interface não é mais necessária pois usamos o tipo do Prisma diretamente
function DashboardContent() {
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
const trpc = useTRPC();
const queryClient = useQueryClient();
const { data: subscriptionData } = useSuspenseQuery(trpc.subscription.getCurrent.queryOptions());
const cancelSubscriptionMutation = useMutation({
...trpc.subscription.cancel.mutationOptions(),
onSuccess: () => {
queryClient.invalidateQueries(trpc.subscription.getCurrent.queryOptions());
},
});
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('pt-BR');
};
return (
<div className="min-h-screen bg-background-50">
<div className="container mx-auto px-4 py-8">
{/* Success Message */}
{sessionId && (
<div className="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg flex items-center">
<CheckCircle className="h-5 w-5 text-green-500 mr-3" />
<div>
<p className="text-green-800 font-medium">Assinatura ativada com sucesso!</p>
<p className="text-green-600 text-sm">Bem-vindo ao MinimalSaaS</p>
</div>
</div>
)}
{/* Welcome Section */}
<div className="mt-8">
<h2 className="text-3xl font-bold mb-2">Bem-vindo de volta!</h2>
<p className="text-background-600">Aqui está o resumo da sua conta</p>
</div>
{/* Subscription Details */}
{subscriptionData && (
<div className="bg-background rounded-lg shadow p-6 mb-8">
<h3 className="text-xl font-semibold mb-4">Detalhes da Assinatura</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p className="text-sm text-background-600">Status</p>
<p className="font-medium capitalize">{subscriptionData.status}</p>
</div>
<div>
<p className="text-sm text-background-600">Cancelamento automático</p>
<p className="font-medium">{subscriptionData.cancelAtPeriodEnd ? 'Sim' : 'Não'}</p>
</div>
<div>
<p className="text-sm text-background-600">Criada em</p>
<p className="font-medium">{formatDate(subscriptionData.createdAt.toString())}</p>
</div>
<div>
<p className="text-sm text-background-600">Última atualização</p>
<p className="font-medium">{formatDate(subscriptionData.updatedAt.toString())}</p>
</div>
<CustomerPortalButton customerId={subscriptionData.stripeCustomerId} />
<CancelSubscriptionButton
onCancel={() => cancelSubscriptionMutation.mutate({
stripeSubscriptionId: subscriptionData.stripeSubscriptionId
})}
isLoading={cancelSubscriptionMutation.isPending}
disabled={subscriptionData.cancelAtPeriodEnd}
/>
</div>
</div>
)}
{/* No Subscription Message */}
{!subscriptionData && (
<div className="bg-background rounded-lg shadow p-6 text-center">
<AlertCircle className="h-12 w-12 text-background-400 mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">Nenhuma assinatura ativa</h3>
<p className="text-background-600 mb-4">
Você não possui uma assinatura ativa no momento.
</p>
<Button asChild>
<Link href="/pricing">Ver Planos</Link>
</Button>
</div>
)}
</div>
</div>
);
}
export default function DashboardPage() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-background-50 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-background-900 mx-auto mb-4"></div>
<p>Carregando...</p>
</div>
</div>
}
>
<DashboardContent />
</Suspense>
);
}
function CancelSubscriptionButton({
onCancel,
isLoading,
disabled
}: {
onCancel: () => void;
isLoading: boolean;
disabled: boolean;
}) {
const [showConfirmation, setShowConfirmation] = useState(false);
const handleCancel = () => {
onCancel();
setShowConfirmation(false);
};
if (disabled) {
return (
<div className="text-sm text-fg-dim">
Assinatura já está programada para cancelamento
</div>
);
}
return (
<div>
{!showConfirmation ? (
<Button
variant="destructive"
size="sm"
onClick={() => setShowConfirmation(true)}
disabled={isLoading}
>
Cancelar Assinatura
</Button>
) : (
<div className="space-y-2">
<p className="text-sm text-fg-dim">
Tem certeza que deseja cancelar sua assinatura?
</p>
<div className="flex gap-2">
<Button
variant="destructive"
size="sm"
onClick={handleCancel}
disabled={isLoading}
>
{isLoading ? 'Cancelando...' : 'Sim, cancelar'}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowConfirmation(false)}
disabled={isLoading}
>
Não, manter
</Button>
</div>
</div>
)}
</div>
);
}
function CustomerPortalButton({ customerId }: { customerId: string }) {
const [loading, setLoading] = useState(false);
const handlePortalAccess = async () => {
setLoading(true);
try {
const response = await fetch('/api/stripe/create-portal-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ customerId }),
});
const { url } = await response.json();
window.location.href = url;
} catch (error) {
console.error('Erro:', error);
} finally {
setLoading(false);
}
};
return (
<Button
onClick={handlePortalAccess}
disabled={loading}
variant="outline"
className="border-background-600"
>
{loading ? 'Carregando...' : '⚙️ Gerenciar Assinatura'}
</Button>
);
}⚡ useSuspenseQuery: O Melhor dos 2 Mundos
🎯 Por que useSuspenseQuery é Superior?
useSuspenseQuery combina as vantagens de Server Components (data fetching automático) com Client Components (interatividade). Elimina loading states manuais e garante que dados estejam sempre disponíveis quando o componente renderiza.
🖥️ Server Components (tRPC Caller)
- ✅ SEO-friendly (renderização no servidor)
- ✅ Performance inicial mais rápida
- ❌ Sem interatividade (onClick, useState)
- ❌ Sem cache do React Query
- ❌ Sem invalidação automática
💻 Client Components (useQuery tradicional)
- ✅ Totalmente interativo
- ✅ Cache e invalidação do React Query
- ❌ Loading states manuais complexos
- ❌ Conditional rendering (`if (isLoading)`)
- ❌ TypeScript mais verboso
🚀 useSuspenseQuery: Melhor dos 2 Mundos
- ✅ Data sempre disponível: Sem `data | undefined`
- ✅ Loading automático: Suspense boundary cuida disso
- ✅ Cache do React Query: Performance otimizada
- ✅ Interatividade completa: Mutations, invalidação
- ✅ TypeScript limpo: Tipos garantidos sem checks
- ✅ Error boundaries: Tratamento de erro centralizado
🔄 Exemplo Prático na Prática
❌ useQuery tradicional:
const { data, isLoading, error } = trpc.subscription.getCurrent.useQuery();
if (isLoading) return <Spinner />;
if (error) return <Error />;
if (!data) return <NoData />;
return <div>{data.status}</div>;✅ useSuspenseQuery:
const { data } = useSuspenseQuery(
trpc.subscription.getCurrent.queryOptions()
);
// data sempre existe! Sem checks condicionais
return <div>{data.status}</div>;✅ Features Implementadas
useSuspenseQuery: Loading automático
useMutation: Cancelar assinatura
Cache Invalidation: UI sempre atualizada
Type Safety: Tipos automáticos do Prisma
Error Handling: Estados de loading
UX Otimizada: Confirmações e feedback
⚡ Passo 12: Resolvendo Performance TypeScript
🚨 Problemas Reais de Performance
Realidade crua: TypeScript tem problemas sérios de performance em projetos grandes com tRPC. Não é performance de runtime - é o Language Server que falha. Desenvolvedores ficam reiniciando o TS Server constantemente com Cmd+Shift+R.
🎯 Por que tRPC Causa Lentidão?
Inferência Excessiva: tRPC usa inferência de tipos massiva. Cada procedure é inferida do input até o output, sem definições manuais.
Problema N×N: Cada mudança recomputa TODOS os routers. Um novo endpoint força re-checagem de centenas de outros.
Cache Quebrado: Project References não funcionam adequadamente com tipos inferidos profundamente.
🔬 A Investigação
Alex (criador do tRPC) e Theo investiram dinheiro para contratar David Sherret (especialista em performance TS) para auditar o tRPC e descobrir o problema.
Descoberta surpreendente: O problema não era no tRPC - era um bug de performance no próprio TypeScript relacionado ao cache de tipos.
🚀 A Solução: TypeScript 5.9+
Resultado: Andarist (contribuidor lendário do TS) criou fix que reduziu instanciações de tipos em 78% em projetos tRPC.
Performance gains: Verificação de tipos 2x mais rápida, menos travamentos do Language Server, experiência mais fluida.
💡 Como Resolver Hoje
1. Upgrade TypeScript
# Instalar TypeScript 5.9+ (tem o fix de performance)
npm install typescript@beta
# Ou usar a versão estável mais recente
npm install typescript@latest
# Verificar versão
npx tsc --version2. Configurar VS Code
{
// Usar versão local do TypeScript
"typescript.preferences.includePackageJsonAutoImports": "off",
"typescript.suggest.autoImports": false,
// Aumentar limite de memory para projetos grandes
"typescript.tsserver.maxTsServerMemory": 8192,
// Desabilitar features pesadas em projetos grandes
"typescript.disableAutomaticTypeAcquisition": true
}3. Otimizar tsconfig.json
{
"compilerOptions": {
// Acelerar compilação
"incremental": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.tsbuildinfo",
// Reduzir work load
"skipLibCheck": true,
"skipDefaultLibCheck": true,
// Module resolution mais eficiente
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true
},
// Excluir arquivos desnecessários
"exclude": [
"node_modules",
"dist",
".next",
"coverage"
]
}🎯 Dicas de Performance para tRPC
✅ Faça
- • Use TypeScript 5.9+ sempre
- • Separe routers por módulo
- • Evite routers muito grandes
- • Use `skipLibCheck: true`
- • Reinicie TS Server quando necessário
❌ Evite
- • Centenas de procedures em um router
- • Tipos muito profundos desnecessários
- • TypeScript muito antigo
- • Project references complexas
- • Definições de tipo manuais
🎉 Estado Atual
Com TypeScript 5.9+, tRPC tem performance muito melhor. O investimento da comunidade resolveu um problema real que afetava milhares de desenvolvedores. Hoje você pode usar tRPC em projetos grandes sem sofrer com travamentos constantes.
✅ Setup Completo Finalizado
🎯 O que Você Conquistou:
- ✅ tRPC configurado com type-safety completo
- ✅ Middleware de autenticação Clerk integrado
- ✅ Context reutilizável com user session
- ✅ Query client otimizado para performance
- ✅ Provider integrado no layout principal
- ✅ Homepage otimizada com componentes shadcn/ui
- ✅ Router de assinaturas com procedures protegidas
- ✅ Dashboard refatorado com tRPC hooks
- ✅ Performance TypeScript otimizada para produção
🚀 Próximos Passos:
- • Criar primeiro router com procedures
- • Implementar CRUD operations
- • Adicionar validação com Zod
- • Integrar com Prisma ORM