Skip to content

⚡ WORKERS COMO BACKEND ÚNICO

Este documento tiene una sola tesis: Workers ES el backend. No existe Express, Fastify, Django, Rails, Laravel, VPS, ni Docker en este stack. Todo lo que un servidor tradicional hace, un Worker lo hace mejor, más barato y sin administración.


🎯 REGLAS INQUEBRANTABLES

[REQUIRED] WORKER-001: Un solo stack de Workers para TODOS los clientes.

Por qué: un backend por plataforma duplica cada regla de negocio tantas veces como clientes existan, y duplicar significa que divergen: el descuento se calcula distinto en la app y en la web. Una sola API para todos los clientes es lo que garantiza que la regla de negocio se aplique una vez, en un solo lugar. La misma API sirve web, mobile (React Native/Expo), desktop (Tauri), IoT y cualquier cliente HTTP. NUNCA Workers separados por plataforma.

[REQUIRED] WORKER-002: NUNCA usar framework de servidor tradicional.

Por qué: los frameworks de servidor tradicionales asumen un proceso persistente con estado en memoria entre peticiones, algo que no existe en el modelo de Workers. Usarlos ahí no es solo desaprovechar la plataforma: partes enteras del framework asumen garantías que el runtime no ofrece, y fallan de formas sutiles bajo carga. Express, Fastify, Koa, Hono (modo servidor), Django, Rails, Laravel, FastAPI — ninguno. El handler fetch() de Workers es el reemplazo. No admite debate.

WORKER-003: Lógica de negocio en Services, NUNCA en el handler. Ver BACKEND_ENGINEERING_STANDARD.md §02 — capas router → middleware → handler → service → datos.

WORKER-004: Comunicación interna SIEMPRE con Service Bindings. Workers internos se llaman con env.SERVICE_NAME.fetch(). NUNCA con HTTP entre Workers propios — eso añade latencia de red innecesaria y expone endpoints internos.


🏗️ ARQUITECTURA: DE DONDE VIENES → A DONDE VAS

❌ ARQUITECTURA TRADICIONAL (lo que se elimina)

Cliente  →  VPS/Docker (Express/Nginx)  →  PostgreSQL / Redis

              ├── SSL manual
              ├── PM2 / systemd
              ├── Nginx config
              ├── Load balancer
              └── Ops team


✅ ARQUITECTURA WORKERS (lo que se adopta)

Cliente  →  api-gateway (Worker público)

                  ├── auth-worker     (Service Binding, interno)
                  ├── docs-worker     (Service Binding, interno)
                  ├── billing-worker  (Service Binding, interno)
                  ├── search-worker   (Service Binding, interno)
                  └── webhook-worker  (público: /webhooks/stripe)

                  └── Infraestructura
                        ├── Supabase   (PostgreSQL + Auth + Realtime)
                        ├── D1         (métricas, logs ligeros)
                        ├── R2         (archivos: imágenes, PDFs)
                        ├── KV         (caché, feature flags, sesiones)
                        ├── Queues     (jobs asíncronos > 15s)
                        └── DO         (WebSockets, rate limiting con estado)

✅ TODO LO QUE UN WORKER PUEDE HACER

FuncionalidadCómo en WorkersReferencia
REST APIfetch() handler con routerBACKEND_ENGINEERING_STANDARD.md §02
GraphQLGraphQL Yoga (Edge-compatible)— [pendiente de documentar]
WebSocketsDurable ObjectsCLOUDFLARE_PLATFORM_STANDARD.md §DO
Autenticación JWTsupabase.auth.getUser(token)BACKEND_ENGINEERING_STANDARD.md §04
Rate LimitingDurable Objects o Upstash RedisESTANDAR_RATE_LIMITING.md
CORSMódulo shared-http/cors.tsBACKEND_ENGINEERING_STANDARD.md §03
ValidaciónZod (mismo que frontend)SECURITY_ENGINEERING_STANDARD.md S-001
CRUD PostgresSupabase client con RLSDATABASE_ENGINEERING_STANDARD.md
File UploadStreaming directo a R2CLOUDFLARE_PLATFORM_STANDARD.md §R2
File DownloadPresigned URLs R2 o streamingCLOUDFLARE_PLATFORM_STANDARD.md §R2
EmailResend + React Email templatesNOTIFICATIONS_STANDARD.md §email
Notificaciones Push WebVAPID + Service WorkerNOTIFICATIONS_STANDARD.md §vapid
Notificaciones Push MobileExpo Push (FCM/APNs)NOTIFICATIONS_STANDARD.md §mobile
Notificaciones In-AppSupabase RealtimeFRONTEND_REALTIME_PATTERN.md
Webhooks (Stripe, GitHub...)Handler público con verificación de firmaEste documento §webhook
PagosStripe SDK (Edge-compatible)— [pendiente de documentar]
PDF Generationpdf-lib (Edge-compatible)PATRON_GENERACION_PDF_EDGE.md
Image ProcessingCloudflare Images API o WASMHEAVY_COMPUTE_STANDARD.md
Cron Jobsscheduled() handler en wranglerEste documento §scheduled
Colas de trabajoCloudflare QueuesCLOUDFLARE_PLATFORM_STANDARD.md §Queues
CachéKV + Cache APIWORKERS_OPTIMIZATION.md
Feature FlagsKV (key = flag, value = JSON)WORKERS_OPTIMIZATION.md
A/B TestingKV + lógica en el handlerWORKERS_OPTIMIZATION.md
SSR / HTML streamingHTMLRewriter o render a string— [pendiente]
Proxy reversofetch() a cualquier API externaEste documento §proxy

❌ LO QUE UN WORKER NO PUEDE HACER (con alternativa)

LimitaciónRestricciónAlternativa
PostgreSQL directoNo hay TCP rawSupabase REST/RPC
CPU > 30 segundosLímite de CPU del runtimeQueues + Container Workers
Archivos > 100MB en bodyLímite de request sizeR2 multipart upload directo
Filesystem localStateless, no hay discoR2 para objetos
GPU / ML inferenceSin GPU en el EdgeCloudflare AI Gateway / Workers AI
WebSockets "nativos"Sin estado entre requestsDurable Objects
Librerías Node.js que usan fs, net, tlsSin APIs de Node en WorkersReemplazar por fetch + R2

📦 ESTRUCTURA DEL BACKEND

apps/
├── api-gateway/              ← ÚNICO punto de entrada público
│   ├── src/
│   │   ├── index.ts          ← fetch() handler principal
│   │   ├── router.ts         ← Dirige a Service Bindings internos
│   │   ├── middleware/
│   │   │   ├── auth.ts       ← JWT verification
│   │   │   ├── rate-limit.ts ← Límites por IP/usuario
│   │   │   └── cors.ts       ← Orígenes permitidos
│   │   └── lib/
│   │       ├── response.ts   ← ok(), fail() — nunca repetidos
│   │       └── circuit-breaker.ts
│   └── wrangler.toml

├── auth-worker/              ← /api/auth/* (registro, login, refresh)
├── docs-worker/              ← /api/documents/* (CRUD documentos)
├── teams-worker/             ← /api/teams/* (equipo, miembros)
├── billing-worker/           ← /api/billing/* (Stripe, suscripciones)
├── notif-worker/             ← interno: email + push
├── search-worker/            ← /api/search/*
├── webhook-worker/           ← /webhooks/* (Stripe, GitHub — público)
└── embeddings-worker/        ← Queue consumer (pgvector embedding jobs)

🔄 MIGRACIÓN DESDE BACKEND TRADICIONAL

typescript
// ❌ ANTES — Express
app.post('/api/proposals', authMiddleware, validateBody, async (req, res) => {
  try {
    const proposal = await db.proposals.create(req.body)
    res.json({ success: true, data: proposal })
  } catch (err) {
    res.status(500).json({ success: false, error: err.message })
  }
})

// ✅ DESPUÉS — Worker (misma lógica, runtime diferente y mejor)
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url)

    if (url.pathname === '/api/proposals' && request.method === 'POST') {
      const corsHeaders = getCorsHeaders(env.ENVIRONMENT, request.headers.get('Origin'))

      // Capa 1: Auth
      const { data: { user }, error: authError } = await supabase.auth.getUser(
        request.headers.get('Authorization')?.replace('Bearer ', '') ?? ''
      )
      if (authError || !user) return fail('UNAUTHORIZED', 'Token inválido', 401, corsHeaders)

      // Capa 2: Validación (Zod)
      const body = await request.json()
      const validated = proposalSchema.parse(body)

      // Capa 3: Persistencia
      const { data, error } = await supabase
        .from('proposals')
        .insert({ ...validated, created_by: user.id })
        .select('id, title, status, created_at')  // DB-001: NUNCA SELECT *
        .single()
      if (error) return fail('DB_ERROR', error.message, 500, corsHeaders)

      return ok(data, corsHeaders)
    }

    return fail('NOT_FOUND', 'Ruta no encontrada', 404, {})
  }
}

La lógica es idéntica. El runtime: Edge global, cold start < 5ms, $0 idle.


⏰ CRON JOBS (Scheduled Workers)

typescript
// wrangler.toml
// [[triggers]]
// crons = ["0 9 * * 1"]  ← Cada lunes a las 9am UTC

export default {
  // Handler HTTP normal
  async fetch(request: Request, env: Env): Promise<Response> { ... },

  // Handler de cron — se ejecuta según la configuración de wrangler.toml
  async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
    ctx.waitUntil(runWeeklyDigest(env))
  }
}

async function runWeeklyDigest(env: Env) {
  // 1. Obtener usuarios con digest activo
  const { data: users } = await supabase
    .from('profiles')
    .select('id, email, display_name')
    .eq('weekly_digest', true)

  // 2. Para cada usuario, encolar el email (no esperar en el cron)
  for (const user of users ?? []) {
    await env.NOTIF_QUEUE.send({
      type: 'weekly-digest',
      userId: user.id,
      email: user.email
    })
  }
}

🔔 WEBHOOKS EXTERNOS (Stripe, GitHub, etc.)

typescript
// apps/webhook-worker/src/stripe.ts
export async function handleStripeWebhook(request: Request, env: Env) {
  const body = await request.text()
  const signature = request.headers.get('stripe-signature') ?? ''

  // OBLIGATORIO: verificar la firma antes de procesar CUALQUIER dato
  let event: Stripe.Event
  try {
    event = stripe.webhooks.constructEvent(body, signature, env.STRIPE_WEBHOOK_SECRET)
  } catch {
    return fail('INVALID_SIGNATURE', 'Firma de webhook inválida', 400, {})
  }

  switch (event.type) {
    case 'customer.subscription.created':
      await handleSubscriptionCreated(event.data.object, env)
      break
    case 'customer.subscription.deleted':
      await handleSubscriptionCanceled(event.data.object, env)
      break
    case 'invoice.payment_failed':
      await handlePaymentFailed(event.data.object, env)
      break
  }

  return ok({ received: true })
}

📋 CHECKLIST DE NUEVO WORKER

Antes de crear un Worker nuevo, verificar:

  • [ ] ¿Tiene su propio wrangler.toml con name, main, compatibility_date?
  • [ ] ¿Tiene solo los bindings que necesita (D1, R2, KV, Queues)?
  • [ ] ¿La ruta que sirve es exclusiva (nunca compartida con otro Worker)?
  • [ ] ¿Los handlers usan ok() y fail() del módulo shared-http?
  • [ ] ¿El CORS viene del módulo shared-http/cors, no redefinido?
  • [ ] ¿La autenticación es un middleware, no copiada en cada handler?
  • [ ] ¿Los logs incluyen traceId propagado desde el gateway?
  • [ ] ¿Los secretos están en las variables de entorno, nunca en el código?

🏆 LO QUE ELIMINAS AL ADOPTAR WORKERS

EliminadoPor quéAhorro anual estimado
VPS ($20-500/mes)Workers escala infinito $0 idle$240-6000
Docker / KubernetesSin contenedores que orquestarTiempo de ops
Nginx / CaddyCloudflare CDN incluido$0 explícito, tiempo de config
PM2 / systemdWorkers no se "caen"Tiempo de ops
SSL renovacionesAutomático en CloudflareTiempo de ops
Load balancers ($15-50/mes)Distribución global automática$180-600
Server monitoringWorkers Analytics incluido$10-50/mes
OS updatesSin servidores que actualizarTiempo de ops

171 documentos indexados · generado desde INDEX.json