Deploy na Cloudflare com Alchemy
Aprenda a fazer deploy do app kubojs em Cloudflare Workers usando Alchemy infrastructure-as-code
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 cloudflareSó web (ex.: com backend Convex):
npm create kubojs@latest my-app \
--frontend tanstack-start \
--backend convex \
--web-deploy cloudflareSó server:
npm create kubojs@latest my-app \
--frontend none \
--backend hono \
--runtime workers \
--server-deploy cloudflareEntendendo 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:
| Framework | Resource Alchemy | Notas |
|---|---|---|
| Next.js | Nextjs | Usa adapter OpenNext |
| Nuxt | Nuxt | Usa preset Nitro Cloudflare |
| SvelteKit | SvelteKit | Usa adapter SvelteKit do Alchemy |
| TanStack Start | TanStackStart | Suporte SSR completo |
| React Router | ReactRouter | Usa adapter Cloudflare do React Router |
| TanStack Router | Vite | Site estático com assets |
| SolidJS | Vite | Site 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/.envIsso 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 }}openssl rand -base64 32Sem 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 prodVariáveis de ambiente:
# packages/infra/.env.prod
ALCHEMY_STAGE=prod
bun run deploy --env-file .env.prodResolução padrão de stage:
- Argumento CLI
--stage - Variável de ambiente
ALCHEMY_STAGE - Variável de ambiente
STAGE - Nome de usuário atual (
$USER) "dev"como fallback
State isolado por stage
Cada stage guarda o state em um diretório separado:
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 deployNo primeiro deploy, o Alchemy vai:
- Criar Cloudflare Workers para web e/ou server
- Criar banco D1 (se configurado)
- Aplicar migrations do banco
- Enviar código e assets
- Logar as URLs de deploy
Modo desenvolvimento
# Runs web and/or server with Alchemy's local emulation
bun run devNo 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 stagingIsso remove:
- Cloudflare Workers
- Bancos D1 (a menos que
delete: falseesteja 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:
- Commitar arquivos de state no repositório (secrets ficam criptografados)
- 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.devAtualizando 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.devSubstitua 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"
- Confira se a variável existe no arquivo
.envcorreto - Verifique se a chamada dotenv
config()carrega esse arquivo - Para secrets, use
alchemy.secret.env.X, nãoalchemy.env.X
"Secret cannot be decrypted" ou "Password required"
- Garanta que
ALCHEMY_PASSWORDestá definido no ambiente - Use a mesma senha que criptografou os secrets
- Confira se a senha não mudou desde o último deploy
"D1 migrations failed"
- Garanta que o path do diretório de migrations está correto em
alchemy.run.ts - Gere ou atualize migrations primeiro:
bun run db:generate(ebun run db:migratepara Prisma) - Confira se os arquivos de migration são SQL válido
"Worker size too large"
- Confira o tamanho do bundle com
bun run build - Habilite minificação na config de build
- Revise dependências — alguns packages não são edge-compatible
"CORS errors in browser"
- Verifique se
CORS_ORIGINbate exatamente com a URL do Worker web - Confira se requests de preflight são tratados
- Garanta que cookies tenham settings
SameSitecorretas