Kubo
Montar stack
Guias

Deploy na Cloudflare com Alchemy

Aprenda a fazer deploy do app kubojs em Cloudflare Workers usando Alchemy infrastructure-as-code

byOscar Gabriel·

Visão geral

Este guia explica como o kubojs usa Alchemy para fazer deploy das suas aplicações em Cloudflare Workers. Você vai aprender:

  • O que são Cloudflare Workers e Alchemy
  • Como fazer deploy de apps web, server ou ambos
  • Como variáveis de ambiente e secrets são gerenciados
  • Como trabalhar com bancos D1
  • Como gerenciar vários stages (dev, prod, staging etc.)
  • Como bindings type-safe funcionam com o package packages/env

O que é Cloudflare Workers?

Cloudflare Workers é uma plataforma serverless que roda seu código na edge network da Cloudflare, em mais de 300 data centers no mundo. Diferente do serverless tradicional (AWS Lambda, Google Cloud Functions), Workers usam isolates V8 em vez de containers. Essa arquitetura oferece cold starts quase zero, distribuição global, definições de tipo TypeScript nativas geradas pelo workerd e suporte full-stack a frameworks como React Router, TanStack Start, SvelteKit e mais.

Workers se integram de forma nativa à plataforma de desenvolvedores da Cloudflare, incluindo bancos D1, object storage R2, stores KV, Durable Objects etc.; tudo acessível via bindings tipados, com free tiers generosos.

O que é Alchemy?

Alchemy é uma biblioteca de Infrastructure-as-Code (IaC). Diferente de Terraform ou Pulumi, o Alchemy é TypeScript puro, baseado em resources, amigável a IA e roda em qualquer lugar onde JS rode. Você define o que quer com funções async normais e o Alchemy cuida da criação, atualização e exclusão de tudo.

Quando você gera um projeto com deploy Cloudflare habilitado, o kubojs cria um arquivo alchemy.run.ts que define toda a infraestrutura como código.

Habilitando deploy na Cloudflare

Ao criar um projeto:

Deploy combinado (web + server):

npm create kubojs@latest my-app \
  --frontend tanstack-router \
  --backend hono \
  --runtime workers \
  --web-deploy cloudflare \
  --server-deploy cloudflare

Só web (ex.: com backend Convex):

npm create kubojs@latest my-app \
  --frontend tanstack-start \
  --backend convex \
  --web-deploy cloudflare

Só server:

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

Entendendo o alchemy.run.ts

O arquivo alchemy.run.ts é o coração da configuração de deploy. Aqui vai um exemplo simplificado de deploy combinado web + server:

// packages/infra/alchemy.run.ts

import alchemy from "alchemy";
import { TanStackStart } from "alchemy/cloudflare";
import { Worker } from "alchemy/cloudflare";
import { D1Database } from "alchemy/cloudflare";
import { config } from "dotenv";

// Load environment variables from multiple .env files
config({ path: "./.env" });
config({ path: "../../apps/web/.env" });
config({ path: "../../apps/server/.env" });

// Initialize the Alchemy app
const app = await alchemy("my-app");

// Create D1 database (if using D1)
const db = await D1Database("database", {
  // Prisma projects use "../../packages/db/prisma/migrations"
  // Drizzle projects use "../../packages/db/src/migrations"
  migrationsDir: "../../packages/db/prisma/migrations",
});

// Deploy web frontend
export const web = await TanStackStart("web", {
  cwd: "../../apps/web",
  bindings: {
    VITE_SERVER_URL: alchemy.env.VITE_SERVER_URL!,
  },
});

// Deploy server backend
export const server = await Worker("server", {
  cwd: "../../apps/server",
  entrypoint: "src/index.ts",
  compatibility: "node",
  bindings: {
    DB: db,
    CORS_ORIGIN: alchemy.env.CORS_ORIGIN!,
    BETTER_AUTH_SECRET: alchemy.secret.env.BETTER_AUTH_SECRET!,
    BETTER_AUTH_URL: alchemy.env.BETTER_AUTH_URL!,
  },
  dev: {
    port: 3000,
  },
});

// Log deployment URLs
console.log(`Web    -> ${web.url}`);
console.log(`Server -> ${server.url}`);

// Finalize (triggers cleanup of orphaned resources)
await app.finalize();

Deploys específicos por framework

O Alchemy oferece resources de deploy otimizados para cada framework de frontend:

FrameworkResource AlchemyNotas
Next.jsNextjsUsa adapter OpenNext
NuxtNuxtUsa preset Nitro Cloudflare
SvelteKitSvelteKitUsa adapter SvelteKit do Alchemy
TanStack StartTanStackStartSuporte SSR completo
React RouterReactRouterUsa adapter Cloudflare do React Router
TanStack RouterViteSite estático com assets
SolidJSViteSite estático com assets

Variáveis de ambiente e secrets

Carregando variáveis de ambiente

O alchemy.run.ts gerado carrega variáveis de ambiente com dotenv:

import { config } from "dotenv";

// Load from multiple locations (order matters - later files override)
config({ path: "./.env" }); // packages/infra/.env
config({ path: "../../apps/web/.env" }); // apps/web/.env
config({ path: "../../apps/server/.env" }); // apps/server/.env

Isso permite:

  • Manter variáveis compartilhadas em packages/infra/.env
  • Manter variáveis específicas do web em apps/web/.env
  • Manter variáveis específicas do server em apps/server/.env

alchemy.env vs alchemy.secret.env

O Alchemy oferece duas formas de acessar variáveis de ambiente para bindings:

Variáveis públicas

Use alchemy.env para valores de configuração não sensíveis. Eles são armazenados em plaintext nos arquivos de state do Alchemy e ficam visíveis em logs:

bindings: {
  CORS_ORIGIN: alchemy.env.CORS_ORIGIN!,
  VITE_SERVER_URL: alchemy.env.VITE_SERVER_URL!,
  STAGE: alchemy.env.STAGE!,
  VERSION: "1.0.0",
}

Use para:

  • URLs e endpoints
  • Feature flags
  • Identificadores de stage/ambiente
  • Configuração pública

Secrets criptografados

Use alchemy.secret.env para valores sensíveis. Eles são criptografados nos arquivos de state do Alchemy com AES-256-GCM:

bindings: {
  BETTER_AUTH_SECRET: alchemy.secret.env.BETTER_AUTH_SECRET!,
  DATABASE_URL: alchemy.secret.env.DATABASE_URL!,
  API_KEY: alchemy.secret.env.API_KEY!,
  STRIPE_SECRET_KEY: alchemy.secret.env.STRIPE_SECRET_KEY!,
}

Use para:

  • API keys e tokens
  • Credenciais de banco
  • Secrets de auth
  • Qualquer dado sensível

Senha do Alchemy

O Alchemy usa uma senha para criptografar e descriptografar secrets. Depois de criar um projeto, atualize a variável de senha em packages/infra/.env.

Para CI/CD (GitHub Actions):

env:
  ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}
Gere uma senha forte: openssl rand -base64 32

Sem ALCHEMY_PASSWORD, qualquer operação envolvendo secrets falha. Guarde essa senha com segurança e nunca a commite no controle de versão.

Note que para apps com Convex como backend, você deve definir secrets diretamente no dashboard do Convex manualmente ou com convex env set a partir de packages/backend.

Deploys multi-stage

O Alchemy suporta deploy em vários stages (ambientes) como development, production, staging e assim por diante. Cada stage tem state e resources isolados.

Argumento de CLI:

# Development (default)
bun run deploy

# Staging
bun run deploy --stage staging

# Production
bun run deploy --stage prod

Variáveis de ambiente:

# packages/infra/.env.prod
ALCHEMY_STAGE=prod

bun run deploy --env-file .env.prod

Resolução padrão de stage:

  1. Argumento CLI --stage
  2. Variável de ambiente ALCHEMY_STAGE
  3. Variável de ambiente STAGE
  4. Nome de usuário atual ($USER)
  5. "dev" como fallback

State isolado por stage

Cada stage guarda o state em um diretório separado:

web.json
server.json

Isso garante isolamento completo entre ambientes.

Nomeação de resources por stage

Use app.stage para criar nomes de resources únicos por ambiente:

const app = await alchemy("my-app");

// Resources include stage in their names
export const server = await Worker("server", {
  name: `${app.name}-${app.stage}-server`, // e.g., "my-app-prod-server"
  // ...
});

export const db = await D1Database("database", {
  name: `${app.name}-${app.stage}-db`, // e.g., "my-app-dev-db"
  // ...
});

Configuração específica por ambiente

const stage = process.env.STAGE || "dev";
const app = await alchemy("my-app", { stage });

// Stage-specific settings
const isProd = app.stage === "prod";

export const server = await Worker("server", {
  // Production gets custom domain, others get workers.dev URLs
  url: !isProd,
  domains: isProd ? ["api.myapp.com"] : undefined,

  bindings: {
    // Different URLs per environment
    CORS_ORIGIN: isProd ? "https://myapp.com" : `https://${app.stage}.myapp.com`,
  },
});

Bindings type-safe

Como bindings funcionam

Quando você define bindings em alchemy.run.ts, eles ficam disponíveis no código do Worker em runtime. O Alchemy fornece type inference para suporte completo a TypeScript.

O padrão env.d.ts

O kubojs gera um arquivo packages/env/env.d.ts que conecta os bindings do Alchemy ao TypeScript:

import { type server } from "@my-app/infra/alchemy.run";

// Infer types from the Worker's bindings
export type CloudflareEnv = typeof server.Env;

declare global {
  type Env = CloudflareEnv;
}

declare module "cloudflare:workers" {
  namespace Cloudflare {
    export interface Env extends CloudflareEnv {}
  }
}

Isso habilita acesso type-safe aos bindings no código do server.

Acessando bindings no código

Com Hono:

import { Hono } from "hono";
import { env } from "cloudflare:workers";

// Access bindings via cloudflare:workers module
const app = new Hono()
  .get("/users", async (c) => {
    // Type-safe access to bindings
    const db = drizzle(env.DB);
    const users = await db.select().from(usersTable);
    return c.json(users);
  })
  .get("/config", (c) => {
    // Access env vars and secrets
    return c.json({
      corsOrigin: env.CORS_ORIGIN,
      stage: env.STAGE,
    });
  });

Com request handler:

import type { server } from "@my-app/infra/alchemy.run";

export default {
  async fetch(request: Request, env: typeof server.Env) {
    // Type-safe access to all bindings
    const value = await env.KV.get("key");
    const apiKey = env.API_KEY;
    return new Response(`Value: ${value}`);
  },
};

Integração com packages/env

O kubojs usa o package packages/env para variáveis de ambiente type-safe. O setup difere entre Cloudflare Workers e runtimes tradicionais.

Para Cloudflare Workers (server.ts)

Ao fazer deploy na Cloudflare, variáveis de ambiente do server vêm de bindings do Worker, não de process.env:

// packages/env/src/server.ts (Cloudflare Workers)

/// <reference path="../env.d.ts" />

// Re-export env from cloudflare:workers module
// Types are defined in env.d.ts based on your alchemy.run.ts bindings
export { env } from "cloudflare:workers";

Isso significa:

  • Sem validação t3-env para env do server (bindings já são type-safe)
  • Tipos vêm do Alchemy via arquivo env.d.ts
  • Valores em runtime são injetados pelos Cloudflare Workers

Para runtimes de backend tradicionais, variáveis de ambiente do server vêm de process.env e usam validação t3-env.

Para web/client (web.ts)

Variáveis de ambiente do client sempre usam t3-env (com Cloudflare ou não):

// packages/env/src/web.ts

import { createEnv } from "@t3-oss/env-core";
import { z } from "zod";

export const env = createEnv({
  clientPrefix: "VITE_",
  client: {
    VITE_SERVER_URL: z.url(),
  },
  runtimeEnv: import.meta.env,
  emptyStringAsUndefined: true,
});

Resources locais

No desenvolvimento, o Alchemy emula o ambiente local com Miniflare. Rodar bun run dev cria bancos SQLite locais que imitam o comportamento real de produção dos resources, para você desenvolver sem fazer deploy.

Você encontra esses resources emulados em .alchemy/miniflare/v3/.

Comandos de deploy

Comandos na raiz

O kubojs adiciona estes scripts ao package.json da raiz:

{
  "scripts": {
    "dev": "...",
    "deploy": "turbo run deploy -F @my-app/infra",
    "destroy": "turbo run destroy -F @my-app/infra"
  }
}

Deploy da aplicação

# Deploy to default stage (your username or "dev")
bun run deploy

# Deploy to a specific stage
bun run deploy --stage prod

# Or from the infra package directly
cd packages/infra && bun run deploy

No primeiro deploy, o Alchemy vai:

  1. Criar Cloudflare Workers para web e/ou server
  2. Criar banco D1 (se configurado)
  3. Aplicar migrations do banco
  4. Enviar código e assets
  5. Logar as URLs de deploy

Modo desenvolvimento

# Runs web and/or server with Alchemy's local emulation
bun run dev

No modo dev, o Alchemy:

  • Emula D1 localmente com Miniflare
  • Fornece URLs locais para teste
  • Faz hot-reload em mudanças

Destruir resources

# Tear down all deployed resources for current stage
bun run destroy

# Destroy a specific stage
bun run destroy --stage staging

Isso remove:

  • Cloudflare Workers
  • Bancos D1 (a menos que delete: false esteja definido)
  • Todos os bindings associados

Aviso: destroy apaga permanentemente seus resources em deploy. Dados do banco serão perdidos a menos que você tenha configurado delete: false ou exportado backups.

Integração contínua

Importante: Por padrão, o Alchemy usa armazenamento de state em arquivos locais. Isso pode causar problemas em CI/CD onde o filesystem é efêmero.

Para CI/CD, você deve:

  1. Commitar arquivos de state no repositório (secrets ficam criptografados)
  2. Usar um state store remoto via resource CloudflareStateStore

Veja a documentação do Alchemy para configurar um state store e montar um pipeline de CI/CD.

Considerações cross-domain

Quando web e server são deployados como Workers separados, eles têm domínios diferentes:

Web:    https://my-app-web.your-subdomain.workers.dev
Server: https://my-app-server.your-subdomain.workers.dev

Atualizando variáveis de ambiente para production

Antes do deploy, atualize as variáveis de ambiente para usar as URLs de production dos Workers:

# apps/web/.env
VITE_SERVER_URL=https://my-app-server.your-subdomain.workers.dev

# apps/server/.env
CORS_ORIGIN=https://my-app-web.your-subdomain.workers.dev
BETTER_AUTH_URL=https://my-app-server.your-subdomain.workers.dev

Substitua your-subdomain pelo subdomain real dos Cloudflare Workers (encontrado no dashboard da Cloudflare em Workers & Pages).

Configuração de cookies para auth

Ao usar Better-Auth com Workers web e server separados, cookies precisam de configuração especial para funcionar entre subdomínios. Em packages/auth/src/auth.ts, configure:

// packages/auth/src/auth.ts
export const auth = betterAuth({
  // ... other config
  session: {
    cookieCache: {
      enabled: true,
      maxAge: 5 * 60, // 5 minutes
    },
  },
  advanced: {
    crossSubDomainCookies: {
      enabled: true,
      domain: ".workers.dev", // Shared domain for cookies
    },
  },
});

A configuração de auth gerada inclui essas settings comentadas. Descomente-as e substitua o domain pelo subdomain real dos workers (ex.: .your-subdomain.workers.dev) ao fazer deploy em production.

Preste atenção especial às settings de CORS ao mudar de stage. Uma configuração que funciona localmente pode falhar em production (e vice-versa) se as origens CORS ou domains de cookie não baterem com as URLs reais de deploy.

Troubleshooting

"Environment variable X is undefined"

  1. Confira se a variável existe no arquivo .env correto
  2. Verifique se a chamada dotenv config() carrega esse arquivo
  3. Para secrets, use alchemy.secret.env.X, não alchemy.env.X

"Secret cannot be decrypted" ou "Password required"

  1. Garanta que ALCHEMY_PASSWORD está definido no ambiente
  2. Use a mesma senha que criptografou os secrets
  3. Confira se a senha não mudou desde o último deploy

"D1 migrations failed"

  1. Garanta que o path do diretório de migrations está correto em alchemy.run.ts
  2. Gere ou atualize migrations primeiro: bun run db:generate (e bun run db:migrate para Prisma)
  3. Confira se os arquivos de migration são SQL válido

"Worker size too large"

  1. Confira o tamanho do bundle com bun run build
  2. Habilite minificação na config de build
  3. Revise dependências — alguns packages não são edge-compatible

"CORS errors in browser"

  1. Verifique se CORS_ORIGIN bate exatamente com a URL do Worker web
  2. Confira se requests de preflight são tratados
  3. Garanta que cookies tenham settings SameSite corretas