Skip to content

Commit dae5faa

Browse files
authored
migrate: Vercel → Cloudflare Pages + Workers (/mes → ~/bin/zsh-5/mes) (#571)
* migrate: Vercel → Cloudflare Pages + Workers para reducir costos ($110 → ~$0-5/mes) - Cambia adaptador Astro de @astrojs/vercel a @astrojs/cloudflare (mode: 'directory') - Agrega dashboard/wrangler.toml con nodejs_compat para compatibilidad total - Crea cloudflare-workers/crons/ Worker que reemplaza los cron jobs de Vercel (claude-code-check cada 30min, health-check cada 1h en vez de cada 15min) - Actualiza CI/CD: Vercel CLI → wrangler pages deploy (requiere CLOUDFLARE_API_TOKEN y CLOUDFLARE_ACCOUNT_ID en GitHub Secrets) - Elimina crons duplicados del proyecto legacy aitmpl (root vercel.json) - Quick win: agrega Cache-Control headers a components.json (86400s), plugins.json, trending-data.json y claude-jobs.json para cortar el costo de bandwidth de $53 Próximos pasos manuales (ver PR description): 1. Crear proyecto en Cloudflare Pages 2. Agregar secrets de Cloudflare en GitHub 3. Migrar env vars de Vercel a Cloudflare Dashboard 4. Deployar cloudflare-workers/crons con wrangler 5. Apuntar dominios a Cloudflare Pages * fix: nodejs_compat_v2 + react-dom/server.node alias para Cloudflare Workers - Cambia compatibility_flags de nodejs_compat a nodejs_compat_v2 - Agrega alias vite react-dom/server → react-dom/server.node para evitar MessageChannel error en module init time en Cloudflare Workers runtime - Agrega ssr.noExternal para bundlear react-dom y aplicar el alias * fix: revert compatibility_flags a nodejs_compat (estándar) nodejs_compat_v2 no es el flag primario. El fix real del MessageChannel error fue el alias vite react-dom/server → react-dom/server.node, no el cambio de flag. * fix: inject Cloudflare Pages runtime env into process.env via middleware Add Astro middleware that copies context.locals.runtime.env (Cloudflare Pages secrets) into process.env before each request, so API routes can use process.env.X as a unified pattern without changing the call sites. Also add || process.env.X fallbacks to all API routes/libs that were using import.meta.env exclusively (build-time only in Vite). Verified on staging: track-download-supabase, health-check, claude-code-check, collections, github/token all return correct responses.
1 parent a17a72b commit dae5faa

17 files changed

Lines changed: 990 additions & 788 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Deploy to Vercel
1+
name: Deploy to Cloudflare Pages
22

33
on:
44
push:
@@ -18,12 +18,19 @@ jobs:
1818
runs-on: ubuntu-latest
1919
environment: production
2020
steps:
21-
- uses: actions/checkout@v6
22-
- uses: actions/setup-node@v6
21+
- uses: actions/checkout@v4
22+
- uses: actions/setup-node@v4
2323
with:
2424
node-version: 22
25+
- name: Install dependencies
26+
run: npm install
27+
working-directory: dashboard
28+
- name: Build
29+
run: npm run build
30+
working-directory: dashboard
2531
- name: Deploy www + app.aitmpl.com
26-
run: npx vercel --prod --yes --token=${{ secrets.VERCEL_TOKEN }}
32+
run: npx wrangler pages deploy dist --project-name=aitmpl-dashboard --commit-dirty=true
33+
working-directory: dashboard
2734
env:
28-
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
29-
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_DASHBOARD_PROJECT_ID }}
35+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
36+
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

cloudflare-workers/crons/index.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* aitmpl-crons — Cloudflare Worker
3+
*
4+
* Replaces Vercel cron jobs by calling the dashboard API endpoints
5+
* on a schedule. Cron: */30 * * * * (claude-code-check) and 0 * * * * (health-check).
6+
*
7+
* Secrets (wrangler secret put):
8+
* DASHBOARD_URL e.g. https://www.aitmpl.com
9+
* TRIGGER_SECRET shared secret sent as Authorization header
10+
*/
11+
12+
export default {
13+
async scheduled(event, env, ctx) {
14+
const base = env.DASHBOARD_URL || 'https://www.aitmpl.com';
15+
const headers = {
16+
'Authorization': `Bearer ${env.TRIGGER_SECRET || ''}`,
17+
'User-Agent': 'aitmpl-crons/1.0',
18+
};
19+
20+
const cron = event.cron;
21+
22+
if (cron === '*/30 * * * *') {
23+
// Every 30 minutes: check for new Claude Code releases
24+
const res = await fetch(`${base}/api/claude-code-check`, { headers });
25+
console.log(`claude-code-check: ${res.status}`);
26+
} else if (cron === '0 * * * *') {
27+
// Every hour: health check (was every 15 min on Vercel)
28+
const res = await fetch(`${base}/api/health-check`, { headers });
29+
console.log(`health-check: ${res.status}`);
30+
}
31+
},
32+
33+
// Manual trigger for testing: GET /trigger?cron=*/30+*+*+*+*
34+
async fetch(request, env) {
35+
const url = new URL(request.url);
36+
37+
if (url.pathname !== '/trigger') {
38+
return new Response('aitmpl-crons worker', { status: 200 });
39+
}
40+
41+
const auth = request.headers.get('Authorization');
42+
if (!env.TRIGGER_SECRET || auth !== `Bearer ${env.TRIGGER_SECRET}`) {
43+
return new Response('Unauthorized', { status: 401 });
44+
}
45+
46+
const cron = url.searchParams.get('cron') || '*/30 * * * *';
47+
await this.scheduled({ cron }, env, {});
48+
return new Response(`Triggered: ${cron}`, { status: 200 });
49+
},
50+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name = "aitmpl-crons"
2+
main = "index.js"
3+
compatibility_date = "2024-09-23"
4+
5+
# Cron triggers:
6+
# - claude-code-check: every 30 minutes (monitors Claude Code npm releases)
7+
# - health-check: every hour (was every 15 min on Vercel, reduced to save invocations)
8+
[triggers]
9+
crons = ["*/30 * * * *", "0 * * * *"]
10+
11+
# Environment variables — set via: wrangler secret put <KEY>
12+
# Required:
13+
# - DASHBOARD_URL: https://www.aitmpl.com (no trailing slash)
14+
# - TRIGGER_SECRET: shared secret for authenticating internal cron calls

dashboard/astro.config.mjs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
import { defineConfig } from 'astro/config';
22
import react from '@astrojs/react';
3-
import vercel from '@astrojs/vercel';
3+
import cloudflare from '@astrojs/cloudflare';
44
import tailwindcss from '@tailwindcss/vite';
55

66
export default defineConfig({
77
site: 'https://www.aitmpl.com',
88
output: 'server',
9-
adapter: vercel(),
9+
adapter: cloudflare({ mode: 'directory' }),
1010
integrations: [react()],
1111
vite: {
1212
plugins: [tailwindcss()],
1313
resolve: {
1414
dedupe: ['react', 'react-dom'],
15+
// Use the Node.js server build of react-dom to avoid MessageChannel
16+
// dependency at module init time in Cloudflare Workers runtime.
17+
alias: [
18+
{ find: /^react-dom\/server$/, replacement: 'react-dom/server.node' },
19+
],
20+
},
21+
ssr: {
22+
// Bundle react-dom so the alias above applies at build time.
23+
noExternal: ['react-dom'],
24+
// Keep Node built-ins external; provided by nodejs_compat_v2.
25+
external: ['node:fs', 'node:path', 'node:url', 'node:stream'],
1526
},
1627
},
1728
});

0 commit comments

Comments
 (0)