Kubo
Montar stack
Guias

Deploy na Vercel

Aprenda a fazer deploy do app kubojs na Vercel usando Vercel Services

byAman Varshney·

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.json gerado 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 /api conecta 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 vercel

Só web (backend fullstack self):

npm create kubojs@latest my-app \
  --frontend next \
  --backend self \
  --web-deploy vercel

Só server:

npm create kubojs@latest my-app \
  --frontend none \
  --backend hono \
  --runtime bun \
  --server-deploy vercel

O 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 /api antes do request chegar ao backend, então uma chamada do browser para /api/rpc/healthCheck chega 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 usam window.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

FrontendFramework do serviceBackendNotas
Next.jsnextjshonoEntrypoint exporta o app para functions
NuxtnuxtjsexpressRoda sem alteração (listen é interceptado)
SvelteKitsveltekitfastifyRoda sem alteração (listen é interceptado)
AstroastroelysiaEntrypoint exporta o app para functions
React Routerreact-routerSSR via preset do framework
TanStack Starttanstack-start
TanStack Routervite
SolidJSvite

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    # production

Sincronize 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 environment

O 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-team

Quando o app dono dessas chaves roda na Vercel (deploys combinados web + server, ou backend fullstack self), o sync não é uma cópia cega:

ChaveComportamentoPor quê
VITE_SERVER_URL / NEXT_PUBLIC_SERVER_URL / PUBLIC_SERVER_URL / NUXT_PUBLIC_SERVER_URLSobrescrita para /apiO browser deve chamar a mesma origem; rewrites fazem o roteamento
CORS_ORIGIN, BETTER_AUTH_URL, NODE_ENVIgnoradasDerivadas 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:vercel roda vercel dev para validar rewrites, route transforms e paridade de env contra a camada de roteamento da plataforma. É mais lento que bun dev; use como ferramenta de checagem, não no dia a dia.
  • bun deploy:check roda vercel 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 mudar vercel.json.

Referência de comandos

ComandoO que faz
bun dev:vercelRoda o ambiente dev de Vercel Services localmente
bun env:previewSincroniza arquivos .env locais com o ambiente preview
bun env:productionSincroniza arquivos .env locais com o ambiente production
bun deploy:checkDry-run de deploy (sem upload)
bun deployCria um deployment de preview
bun deploy:prodDeploy 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.

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.