---
id: t3-fullstack-typesafe-stack
domain: architecture
agents: [architect]
when: "ao montar um app full-stack type-safe do zero (especialmente para quem não é programador experiente)"
---

# T3 Stack — full-stack type-safe do tipo na ponta, sem escrever tipo

A T3 stack (`create-t3-app`) é o scaffolder que entrega o tipo fluindo do banco até o
`onClick` do botão **sem você declarar nenhuma interface compartilhada**. Você muda a coluna
de uma tabela; o front-end quebra no `tsc` na hora, no campo certo. Esse é o produto: não é um
"template bonito", é uma **arquitetura de boundaries tipados** que deixa um não-programador
montar algo sólido porque o compilador vira o revisor.

## O problema

Quem está montando um app full-stack do zero cai em três armadilhas que a T3 resolve por
construção:

1. **O contrato API duplicado e dessincronizado.** O back-end retorna `{ userId }`, o front
   espera `{ user_id }`, e ninguém percebe até dar `undefined` em produção. A solução ingênua
   (escrever tipos à mão em dois lugares, ou um OpenAPI/GraphQL gigante) é trabalho que
   apodrece. A T3 elimina o contrato escrito: o tipo da API **é inferido** do código do
   servidor.
2. **O env var que falta só aparece em produção.** `process.env.STRIPE_KEY` é `string |
   undefined` em todo lugar, e o deploy sobe sem a chave. A T3 valida env no build e te dá
   tipos.
3. **A escolha de stack que trava você.** "Uso Prisma ou Drizzle? Pages ou App Router? Que
   banco?" Sem critério, você escolhe por hype. A T3 expõe **uma flag por decisão**, com
   default seguro, e te força a entender o trade-off de cada peça em vez de copiar um boilerplate
   opaco.

A filosofia oficial é dura e vale citar porque ela explica *por que* a stack é montada assim:
**"Typesafety isn't optional. Any decision that compromises the typesafe nature of Create T3
App is a decision that should be made in a different project."** A consequência prática: a T3
**não** vem com gerenciador de estado, biblioteca de componentes, ou solução de deploy. **"We
expect you to bring your own libraries that solve the needs of YOUR application."** Cada peça
que está lá resolve um problema específico das tecnologias-core — nada de bloat.

## O conhecimento

### As peças e o que CADA uma resolve

| Peça | Resolve | Por que está na T3 (e não outra) |
|---|---|---|
| **Next.js (App Router)** | Roteamento + Server/Client Components + bundling | Base; Server Components deixam chamar a API no servidor sem round-trip HTTP |
| **TypeScript** | A linguagem do contrato | É o eixo: tudo existe pra preservar a inferência ponta a ponta |
| **tRPC** | API type-safe sem schema escrito | Tipo flui por inferência (`typeof appRouter`), sem codegen, sem OpenAPI. Switching cost baixo = "bleed responsibly" |
| **Zod** | Validação no boundary (input da API + env) | Schema único que **é** runtime-validator **e** fonte do tipo TS |
| **Prisma** *ou* **Drizzle** | ORM type-safe (escolha uma) | Prisma = DX mais alta, schema próprio. Drizzle = mais perto do SQL, sem engine binário |
| **NextAuth.js** | Auth (sessão, OAuth, adapter de banco) | "When you need flexible, secure, scalable auth" — integra no banco que você já tem |
| **Tailwind CSS** | Estilo utility-first | Opt-in; não impõe design system |
| **@t3-oss/env-nextjs** | Env vars validadas com Zod no build | Build não passa sem as env vars necessárias |

> Filosofia "**Bleed responsibly**": tecnologia mais nova só entra onde o custo de troca é
> baixo (tRPC, sim; banco experimental, não).

### As flags do `create-t3-app` — uma decisão por flag

Modo interativo (recomendado pra aprender): `npm create t3-app@latest`. Cada prompt é uma
decisão arquitetural explícita. Para automação existe o modo CI.

**Flags gerais (sempre valem):**

| Flag | O que faz |
|---|---|
| `[dir]` | Nome/diretório do projeto |
| `-y`, `--default` | Bootstrap com **todas** as opções selecionadas, pula os prompts |
| `--noGit` | Não inicializa repositório git |
| `--noInstall` | Gera o projeto **sem** rodar o install de dependências |

**Flags experimentais de CI (só funcionam junto com `--CI`):**

| Flag | O que ativa |
|---|---|
| `--CI` | Liga o modo não-interativo; **sem ela, as flags abaixo não têm efeito** |
| `--trpc` | Inclui tRPC (API type-safe) |
| `--prisma` | Inclui Prisma como ORM |
| `--drizzle` | Inclui Drizzle como ORM (alternativa ao Prisma — escolha **uma**) |
| `--nextAuth` | Inclui NextAuth.js |
| `--tailwind` | Inclui Tailwind CSS |
| `--appRouter` | Usa o App Router do Next.js (em vez do Pages Router) |
| `--dbProvider [provider]` | Configura o banco. Valores: `mysql`, `postgres`, `planetscale`, `sqlite`. **Default: `sqlite`** |

Exemplos reais da doc:
```bash
pnpm dlx create-t3-app@latest --CI --trpc --tailwind
pnpm dlx create-t3-app@latest --CI --nextAuth --tailwind --drizzle --dbProvider postgres
```

> Regra: você **não** precisa opt-out do que não quer — basta não passar a flag. Se quiser ser
> explícito, passe `false` (ex.: `--nextAuth false`).

### A organização de pastas (App Router) — `src/server` é a parede

O eixo da arquitetura é **uma fronteira física entre o que roda no servidor e o que vai pro
browser**. Isso não é estético: é o que garante que código de servidor (e segredos) nunca
vaze pro bundle do cliente.

```
prisma/ (ou drizzle.config.ts)
  schema.prisma          # conexão + schema do banco, migrations, seed
public/
  favicon.ico            # assets estáticos
src/
  env.js                 # validação e tipos das env vars (Zod)
  app/                   # TODAS as rotas do Next (page.tsx, layout.tsx com providers)
    _components/         # componentes da app
  server/                # código que SÓ roda no servidor (a "parede")
    api/
      trpc.ts            # config principal do back-end tRPC (context + helpers de procedure)
      root.ts            # faz merge dos routers e exporta o router + o TIPO dele
      routers/           # sub-routers (post.ts, user.ts, ...)
    auth/                # config do NextAuth (config.ts)
    db/                  # cliente Drizzle/Prisma (index.ts) + schema (schema.ts)
  trpc/                  # ponte pra CHAMAR tRPC do cliente e dos server components
    server.ts            # entrypoint pra usar tRPC em Server Components
    react.tsx            # entrypoint front-end do tRPC (tipos de input/output do router)
    query-client.ts      # cria o Query Client (cache + dedupe nos client components)
  styles/
postcss.config.js        # Tailwind via PostCSS
```

Regra de ouro: **nada em `src/app` importa de `src/server`**. O cliente importa só o **tipo**
`AppRouter` — nunca a implementação. É isso que mantém o type-safety sem mandar o servidor pro
browser.

### tRPC — o tipo flui por inferência, não por contrato escrito

**1. Um sub-router define procedures. Input validado com Zod:**
```typescript
const userRouter = createTRPCRouter({
  getById: publicProcedure.input(z.string()).query(({ ctx, input }) => {
    return ctx.prisma.user.findFirst({
      where: { id: input },
    });
  }),
});
```

**2. `root.ts` junta os sub-routers e exporta SÓ o tipo:**
```typescript
const appRouter = createTRPCRouter({
  users: userRouter,
  posts: postRouter,
  messages: messageRouter,
});

export type AppRouter = typeof appRouter;   // <- isto, e só isto, vai pro cliente
```

**3. O cliente chama com type-safety total (input e output inferidos):**
```typescript
const userQuery = api.users.getById.useQuery(query.id);
```
Erre o nome do campo, ou mude o retorno no servidor, e o `tsc` acusa **aqui**. Sem codegen,
sem step de build, sem schema duplicado.

**O context (`trpc.ts`) tem duas camadas:**
- `createInnerTRPCContext` — o que **não** depende da request (ex.: conexão de banco). Útil pra
  testes.
- `createTRPCContext` — o que **depende** da request (ex.: a sessão do usuário).

### `publicProcedure` vs `protectedProcedure` — auth é um middleware

`protectedProcedure` é só um `publicProcedure` com um middleware que checa a sessão (que veio
do context). Se não há usuário, dispara `UNAUTHORIZED` **antes** do seu resolver rodar:
```typescript
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({ /* ctx com session garantida não-nula */ });
});
```
Dentro de uma `protectedProcedure`, `ctx.session.user` é tipado como existente. Você não
escreve `if (!user) return 401` em cada handler — a parede já é o tipo.

### NextAuth — sessão no servidor e no cliente

| Onde | Como pega a sessão |
|---|---|
| Server Component (App Router) | `const session = await auth();` (de `~/server/auth`) |
| Client Component | `const { data: session } = useSession();` (de `next-auth/react`, precisa `SessionProvider` no topo) |
| Dentro de uma procedure tRPC | `ctx.session` (injetada via `createTRPCContext`) |

- **Adapter de banco:** ao escolher Prisma **+** NextAuth (ou Drizzle + NextAuth), você ganha um
  sistema de auth funcional com **todos os models pré-configurados** (User/Account/Session).
- **Provider padrão:** Discord OAuth. Você só preenche `DISCORD_CLIENT_ID` /
  `DISCORD_CLIENT_SECRET` no `.env` e registra o callback `<app url>/api/auth/callback/discord`.
- **Adicionar campo na sessão** (ex.: `role`): use module augmentation em
  `server/auth/config.ts` e inclua o campo no callback de sessão:
  ```typescript
  declare module "next-auth" {
    interface Session extends DefaultSession {
      user: { id: string } & DefaultSession["user"];
    }
  }
  ```
  Se adicionar coluna nova no model do adapter, **dê um default** (ex.: `role Role
  @default(USER)`) — o adapter não conhece o campo extra.

### Env vars — o Zod também guarda a fronteira do ambiente

`src/env.js` define server, client e o mapeamento `runtimeEnv`:
```javascript
export const env = createEnv({
  server: {
    NODE_ENV: z.enum(["development", "test", "production"]),
  },
  client: {
    // NEXT_PUBLIC_CLIENTVAR: z.string(),
  },
  runtimeEnv: {
    NODE_ENV: process.env.NODE_ENV,
  },
});
```
Regras:
- Variável de servidor vai em `server`; variável que o browser pode ver **precisa** do prefixo
  `NEXT_PUBLIC_` e vai em `client`.
- Acessar variável de servidor no cliente dispara erro de runtime — a fronteira é tipada.
- **O build não completa** sem as env vars validadas (validação no build *e* runtime).
- Para cada nova var: (1) põe no `.env`, (2) define o validador Zod, (3) destrutura em
  `runtimeEnv` como `KEY: process.env.KEY`. "By destructuring it manually, you ensure that the
  variable will never be stripped out from the bundle."
- Conversão de tipo: `z.coerce.number()`, `z.coerce.boolean()` (env é sempre string crua).

## Checklist

Antes de considerar o scaffold "pronto pra construir em cima":

- [ ] Sei justificar **cada flag** que liguei (não rodei `--default` no automático sem
      entender)?
- [ ] O cliente importa **só o tipo** `AppRouter` — nenhum import de `src/server` em `src/app`?
- [ ] Todo input de procedure tem `.input(z.…)` — nenhum `query`/`mutation` recebe dado cru não
      validado?
- [ ] Rotas que exigem login usam `protectedProcedure`, não `publicProcedure` + check manual?
- [ ] Toda env var que uso está declarada nos três lugares (`.env`, schema Zod, `runtimeEnv`)?
- [ ] Variável sensível **não** está com prefixo `NEXT_PUBLIC_` (senão vaza pro bundle)?
- [ ] Escolhi **um** ORM (Prisma OU Drizzle), não os dois?
- [ ] O `dbProvider` está coerente com o banco real do deploy (não ficou em `sqlite` default por
      esquecimento)?
- [ ] Defini default para qualquer coluna nova que adicionei nos models do adapter NextAuth?

## Tabela de decisão

| Sua necessidade | A peça / flag | Observação |
|---|---|---|
| API type-safe sem escrever contrato | `--trpc` | O retorno do servidor vira o tipo do cliente por inferência |
| Validar input da API e env vars | Zod (vem junto) | Mesmo schema = validação runtime + tipo TS |
| ORM com DX máxima, schema declarativo | `--prisma` | Engine própria; ótimo pra quem está aprendendo |
| ORM perto do SQL, sem binário, edge-friendly | `--drizzle` | Mais controle; `drizzle.config.ts` + `db/schema.ts` |
| Banco de dev rápido, zero setup | `--dbProvider sqlite` | Default; troque para `postgres`/`mysql` no deploy |
| Postgres / MySQL / PlanetScale em prod | `--dbProvider postgres\|mysql\|planetscale` | Defina **antes** de gerar migrations |
| Login, OAuth, sessão | `--nextAuth` | Combine com `--prisma`/`--drizzle` → models de auth prontos |
| Proteger uma rota de API | `protectedProcedure` | Middleware de sessão, não check manual por handler |
| Buscar dados no servidor sem HTTP | `src/trpc/server.ts` em Server Component | Chama a procedure direto, sem round-trip |
| Buscar/mutar dados no cliente com cache | `api.x.y.useQuery/useMutation` (`trpc/react.tsx`) | Cache + dedupe via `query-client.ts` |
| Estilo utility-first | `--tailwind` | Opt-in; sem design system imposto |
| Gerenciador de estado / UI lib / deploy | **nenhuma flag** | Filosofia: "bring your own" — adicione você |
| Automatizar a geração (CI/script) | `--CI` + flags | Sem `--CI`, as flags de pacote são ignoradas |

---

**Fontes (verificadas na doc oficial):** create.t3.gg —
[Introduction](https://create.t3.gg/en/introduction),
[Installation](https://create.t3.gg/en/installation),
[Folder Structure (App)](https://create.t3.gg/en/folder-structure-app),
[tRPC](https://create.t3.gg/en/usage/trpc),
[NextAuth.js](https://create.t3.gg/en/usage/next-auth),
[Environment Variables](https://create.t3.gg/en/usage/env-variables).
