Standalone Workers
CruzJS applications run on Cloudflare Pages, but some workloads are better served by standalone Workers. Workers live in the external-processes/ directory and deploy alongside your main application.
When to Use Workers vs Pages
Section titled “When to Use Workers vs Pages”| Use Case | Pages (main app) | Standalone Worker |
|---|---|---|
| Web UI + API | Yes | No |
| Background processing | Via Queues/Workflows | Direct |
| Scheduled tasks (cron) | No | Yes |
| WebSocket servers | Limited | Yes |
| Custom routing logic | React Router | Full control |
| Independent scaling | No | Yes |
| Separate deployment | No | Yes |
Use a standalone Worker when you need independent deployment, cron triggers, WebSocket support, or processing that should not run inside the Pages request lifecycle.
Scaffolding a Worker
Section titled “Scaffolding a Worker”cruz new worker my-processorThis creates external-processes/my-processor/ with:
external-processes/my-processor/ src/ index.ts # Worker entry point wrangler.toml # Worker configuration package.json tsconfig.jsonWorker Structure
Section titled “Worker Structure”The scaffolded Worker follows Cloudflare’s module Worker syntax:
export interface Env { DB: D1Database; CACHE_KV: KVNamespace; MY_QUEUE: Queue;}
export default { /** * HTTP fetch handler -- responds to incoming requests */ async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const url = new URL(request.url);
if (url.pathname === '/health') { return new Response('OK', { status: 200 }); }
if (url.pathname === '/process' && request.method === 'POST') { const body = await request.json();
// Do work... ctx.waitUntil(processInBackground(body, env));
return Response.json({ status: 'accepted' }); }
return new Response('Not Found', { status: 404 }); },
/** * Scheduled handler -- runs on cron triggers */ async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> { console.log(`Cron trigger: ${event.cron}`); ctx.waitUntil(runScheduledTask(env)); },};
async function processInBackground(data: unknown, env: Env) { // Access D1, KV, or other bindings const result = await env.DB.prepare('SELECT COUNT(*) as count FROM Job').first(); console.log('Job count:', result?.count);}
async function runScheduledTask(env: Env) { // Periodic cleanup, report generation, etc.}wrangler.toml Configuration
Section titled “wrangler.toml Configuration”name = "my-app-my-processor"main = "src/index.ts"compatibility_date = "2024-01-01"
# Share the same D1 database as the main app[[d1_databases]]binding = "DB"database_name = "my-app-db"database_id = "your-database-id"
# Share KV namespace[[kv_namespaces]]binding = "CACHE_KV"id = "your-kv-id"
# Cron triggers (optional)[triggers]crons = ["0 */6 * * *"] # Every 6 hours
# Environment variables[vars]ENVIRONMENT = "production"Sharing Bindings
Section titled “Sharing Bindings”Standalone Workers can bind to the same D1 databases, KV namespaces, R2 buckets, and Queues as your main Pages application. Use the same database_id, id, or bucket_name values from your main wrangler.toml.
Service Bindings
Section titled “Service Bindings”Workers can call each other directly (without HTTP) using Service Bindings:
# In the main app's wrangler.toml[[services]]binding = "PROCESSOR"service = "my-app-my-processor"// In the main appconst response = await env.PROCESSOR.fetch( new Request('https://internal/process', { method: 'POST', body: JSON.stringify({ taskId: '123' }), }));Service Bindings are zero-latency since communication happens within Cloudflare’s network without a public HTTP roundtrip.
Deployment
Section titled “Deployment”Standalone Workers deploy automatically with cruz deploy:
# Deploy everything (main app + all external processes)cruz deploy production
# Preview deploycruz deploy previewThe CLI discovers all directories in external-processes/ with a wrangler.toml and deploys each one.
You can also deploy a single Worker manually:
cd external-processes/my-processornpx wrangler deployAccessing the Database from Workers
Section titled “Accessing the Database from Workers”Since Workers don’t use the CruzJS DI container, use Drizzle directly:
import { drizzle } from 'drizzle-orm/d1';import { jobs } from './schema'; // Copy or import your schemaimport { eq } from 'drizzle-orm';
export default { async fetch(request: Request, env: Env): Promise<Response> { const db = drizzle(env.DB);
const pendingJobs = await db .select() .from(jobs) .where(eq(jobs.status, 'PENDING')) .limit(10);
return Response.json({ jobs: pendingJobs }); },};Testing Workers
Section titled “Testing Workers”Test Workers locally with wrangler:
cd external-processes/my-processornpx wrangler devThis starts a local development server for the Worker with simulated D1, KV, and R2 bindings.
Next Steps
Section titled “Next Steps”- Workflows — Durable multi-step execution
- Queues — Asynchronous message processing
- Deployment — Deploy Workers alongside your app