Deploy na Vercel
Aprenda a fazer deploy do app kubojs na Vercel usando Vercel Services
Visão geral
Este guia explica como o kubojs faz deploy das suas aplicações na Vercel usando Vercel Services. Você vai aprender:
- O que são Vercel Services e como o
vercel.jsongerado funciona - Como fazer deploy de apps web, server ou ambos a partir de um monorepo
- Como variáveis de ambiente são sincronizadas e derivadas
- Como o roteamento same-origin
/apiconecta os serviços web e server - Como verificar um deploy antes de enviar qualquer coisa
O que são Vercel Services?
Vercel Services permitem rodar vários apps — um frontend e um ou mais backends — como serviços dentro de um único projeto Vercel. Cada serviço tem seu próprio build e suas próprias functions Fluid Compute, compartilhando um domínio, um conjunto de variáveis de ambiente e um ciclo de vida de deploy.
O kubojs usa isso para fazer deploy de apps/web e apps/server do monorepo juntos: o serviço web serve o frontend, o serviço server roda o backend Hono/Express/Fastify/Elysia, e rewrites roteiam requests /api/* entre eles no mesmo domínio. Sem configuração de CORS, sem projetos separados para manter em sincronia.
Habilitando deploy na Vercel
Ao criar um projeto:
Deploy combinado (web + server):
npm create kubojs@latest my-app \
--frontend tanstack-router \
--backend hono \
--runtime bun \
--web-deploy vercel \
--server-deploy vercelSó web (backend fullstack self):
npm create kubojs@latest my-app \
--frontend next \
--backend self \
--web-deploy vercelSó server:
npm create kubojs@latest my-app \
--frontend none \
--backend hono \
--runtime bun \
--server-deploy vercelO deploy de server exige runtime bun ou node e um backend separado (hono, express, fastify, elysia). Para o runtime Cloudflare workers, use --server-deploy cloudflare em vez disso.
Entendendo o vercel.json
O vercel.json gerado define seus serviços e o roteamento. Um deploy combinado web + server fica assim:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"bunVersion": "1.x",
"services": {
"web": {
"root": "apps/web",
"framework": "vite",
"installCommand": "cd ../.. && bun install",
"buildCommand": "VITE_SERVER_URL=/api bun run build",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
},
"server": {
"root": "apps/server",
"framework": "hono",
"entrypoint": "src/index.ts",
"installCommand": "cd ../.. && bun install",
"routes": [
{
"src": "/api/((?!auth(?:/|$)).*)",
"transforms": [{ "type": "request.path", "op": "set", "args": "/$1" }]
}
]
}
},
"rewrites": [
{ "source": "/api/(.*)", "destination": { "service": "server" } },
{ "source": "/(.*)", "destination": { "service": "web" } }
]
}Como as peças se encaixam:
- Rewrites enviam
/api/*para o serviço server e todo o resto para o serviço web. A ordem importa — a regra mais específica ganha. - O route transform remove o prefixo
/apiantes do request chegar ao backend, então uma chamada do browser para/api/rpc/healthCheckchega ao server como/rpc/healthCheck— exatamente onde os handlers tRPC/oRPC estão montados. Rotas better-auth são a exceção:/api/auth/*passa sem strip, porque better-auth exige que o path público da URL bata exatamente com o mount no server. Seu código de backend não precisa de paths específicos da Vercel. - O buildCommand do web embute a URL pública do server como
/api, para o client chamar o mesmo domínio de onde foi servido. Os clients oRPC/tRPC gerados normalizam esse path relativo para uma URL absoluta em runtime (browsers usamwindow.location.origin; SSR usa a URL do próprio deployment). - installCommand roda o gerenciador de pacotes na raiz do monorepo para as dependências do workspace resolverem.
Suporte a frameworks
| Frontend | Framework do service | Backend | Notas |
|---|---|---|---|
| Next.js | nextjs | hono | Entrypoint exporta o app para functions |
| Nuxt | nuxtjs | express | Roda sem alteração (listen é interceptado) |
| SvelteKit | sveltekit | fastify | Roda sem alteração (listen é interceptado) |
| Astro | astro | elysia | Entrypoint exporta o app para functions |
| React Router | react-router | SSR via preset do framework | |
| TanStack Start | tanstack-start | ||
| TanStack Router | vite | ||
| SolidJS | vite |
Projetos Astro usam automaticamente o adapter @astrojs/vercel em vez do adapter Node standalone.
Escolhendo uma região
Functions fazem deploy em Washington, D.C. (iad1) por padrão. Se o banco estiver em outro lugar, cada query paga ida e volta entre regiões — uma página SSR com algumas queries pode gastar centenas de milissegundos só de rede. Defina a região onde o banco fica (assets estáticos são servidos do CDN da Vercel perto dos usuários de qualquer forma):
{
"regions": ["sin1"]
}regions é top-level em vercel.json e se aplica a todos os serviços. IDs comuns: iad1 (Washington), sfo1 (San Francisco), fra1 (Frankfurt), sin1 (Singapore), bom1 (Mumbai), syd1 (Sydney) — lista completa. Escolha a mesma região do setup de banco (Neon, Prisma Postgres, Turso etc.).
Fazendo deploy
O fluxo completo de um scaffold novo até produção:
cd my-app
# 1. Authenticate (first time only)
bunx vercel login
# 2. Link the repo to a Vercel project (creates .vercel/project.json)
bun deploy:setup
# 3. (Optional) Dry-run: preview framework detection and files without uploading
bun deploy:check
# 4. Sync envs for the environment you deploy to, then deploy.
# Preview and production are separate env stores on Vercel.
bun env:preview && bun deploy # preview
bun env:production && bun deploy:prod # productionSincronize as variáveis de ambiente antes do primeiro deploy em cada ambiente — preview e
production são stores separados. A Vercel nunca envia arquivos .env locais, então um deploy sem
envs sincronizadas falha na validação de ambiente em runtime.
Se sua conta Vercel pertence a vários times, vercel link --yes precisa de um scope explícito:
bunx vercel link --yes --scope your-team.
Variáveis de ambiente
Há três camadas. Os arquivos .env locais continuam sendo a fonte da verdade, e um comando os projeta na Vercel.
1. Arquivos .env locais
Nada muda em relação a um app kubojs padrão: apps/web/.env guarda vars públicas do client, apps/server/.env guarda secrets, e packages/env valida ambos com zod para valores ausentes falharem de forma ruidosa.
2. Sincronizando com a Vercel
bun env:preview # sync to the preview environment
bun env:production # sync to the production environmentO script gerado scripts/sync-vercel-env.ts faz parse dos seus arquivos .env e envia cada chave com a CLI da Vercel. Como web e server são serviços em um projeto, eles compartilham um store de ambiente — um único sync cobre os dois. Flags extras da CLI passam direto:
bun env:production --scope your-teamQuando o app dono dessas chaves roda na Vercel (deploys combinados web + server, ou backend fullstack self), o sync não é uma cópia cega:
| Chave | Comportamento | Por quê |
|---|---|---|
VITE_SERVER_URL / NEXT_PUBLIC_SERVER_URL / PUBLIC_SERVER_URL / NUXT_PUBLIC_SERVER_URL | Sobrescrita para /api | O browser deve chamar a mesma origem; rewrites fazem o roteamento |
CORS_ORIGIN, BETTER_AUTH_URL, NODE_ENV | Ignoradas | Derivadas em runtime a partir da URL do deployment (veja abaixo) |
Todo o resto (DATABASE_URL, secrets, …) | Copiado como está |
3. Derivação em runtime
O env gerado do server deriva CORS_ORIGIN e BETTER_AUTH_URL das variáveis da própria Vercel quando não estão definidas: deploys de production usam VERCEL_PROJECT_PRODUCTION_URL, previews usam VERCEL_URL. É por isso que o sync as ignora — cada deployment, inclusive previews, se configura sozinho com a origem certa e zero setup por ambiente.
Valores sincronizados são armazenados como Sensitive por padrão, o que significa que vercel env pull os mostra como strings vazias. Eles ainda ficam disponíveis para builds e functions —
isso é comportamento esperado da Vercel, não falha de sync.
Depois de mudar envs, rode o sync de novo e faça redeploy — mudanças de ambiente só valem no próximo deployment.
Desenvolvimento local
bun dev continua sendo seu loop principal — hot reload nativo, sem envolvimento da Vercel. Dois comandos específicos da Vercel complementam:
bun dev:vercelrodavercel devpara validar rewrites, route transforms e paridade de env contra a camada de roteamento da plataforma. É mais lento quebun dev; use como ferramenta de checagem, não no dia a dia.bun deploy:checkrodavercel deploy --dry— imprime os presets de framework detectados e os arquivos exatos que um deployment incluiria, sem enviar nada. Útil antes do primeiro deploy ou depois de mudarvercel.json.
Referência de comandos
| Comando | O que faz |
|---|---|
bun dev:vercel | Roda o ambiente dev de Vercel Services localmente |
bun env:preview | Sincroniza arquivos .env locais com o ambiente preview |
bun env:production | Sincroniza arquivos .env locais com o ambiente production |
bun deploy:check | Dry-run de deploy (sem upload) |
bun deploy | Cria um deployment de preview |
bun deploy:prod | Deploy em production |
Se um projeto faz deploy de web e server em plataformas diferentes (Vercel + Cloudflare), os
scripts de deploy são nomeados por alvo: deploy:web / deploy:web:prod para um lado e
deploy:server para o outro. Scripts de setup, sync de env e dry-run mantêm os mesmos nomes.
Troubleshooting
"Environment variable X is undefined" depois do deploy
Você fez deploy antes de sincronizar as envs. Rode bun env:production (ou :preview) e faça redeploy.
vercel link --yes falha com "Provide --team or --scope"
Sua conta pertence a vários times. Passe o scope explicitamente: bunx vercel link --yes --scope your-team.
vercel env pull retorna valores vazios
Variáveis sincronizadas são armazenadas como Sensitive e não podem ser lidas de volta. Elas ainda funcionam em builds e em runtime. Confira com vercel env ls production para confirmar que existem.
Chamadas de API falham com erros de CORS (deploy só server)
Com deploy só de server, o app web vive em outro domínio, então o CORS_ORIGIN derivado em runtime (a própria origem do server) fica errado. Defina CORS_ORIGIN com a URL do app web em apps/server/.env e sincronize de novo.
Chamadas RPC de SSR retornam 401 em deployments de preview
RPC no browser sempre funciona — chama a mesma origem de onde a página foi servida. Mas RPC no server (frameworks SSR como Next.js ou TanStack Start) reconstrói uma URL absoluta a partir de VERCEL_URL, que passa pela URL pública do deployment. Com Deployment Protection habilitado (Standard Protection é o padrão da Vercel), URLs de preview exigem autenticação, então esses fetches server-to-server recebem 401. Production não é afetada — o domínio de production não é protegido.
Workarounds: defina um secret de Protection Bypass for Automation e envie-o em fetches do server, relaxe a proteção de previews nas configurações do projeto, ou evite RPC no server em deployments de preview. Service bindings (URLs privadas service-to-service) são o fix de longo prazo e estão no roadmap deste template.
O browser chama /api mas recebe o 404 do app web
Confira se o vercel.json ainda contém os dois rewrites na ordem — /api/(.*) para o serviço server deve vir antes do catch-all. O dry-run (bun deploy:check) mostra o que a Vercel detectou.
Mudei uma env mas o app ainda vê o valor antigo
Envs são lidas no build/boot. Rode o sync de env de novo e faça redeploy.