LumenizeDO
LumenizeDO is the base class for stateful mesh nodes running as Cloudflare Durable Objects. For a hands-on introduction, see the Getting Started Guide.
Mesh API
LumenizeDO shares the standard Mesh API with all node types — this.lmz for identity and calls, @mesh() decorator for entry points, onBeforeCall() for access control, and this.ctn<T>() for continuations.
NADIS Auto-Injection
NADIS (Not A DI System) provides zero-boilerplate dependency injection via this.svc.
How It Works
Services are accessed via this.svc. Some are built-in, others require a side-effect import:
class MyDO extends LumenizeDO<Env> {
doSomething() {
this.svc.sql`SELECT * FROM items`; // ✅ Built-in
this.svc.alarms.schedule(60, this.ctn().handleTask({ id: 1 })); // ✅ Built-in
}
}
Available Services
| Package | Service | Import Required | Purpose |
|---|---|---|---|
@lumenize/mesh | sql | No (built-in) | SQL template literal tag |
@lumenize/mesh | alarms | No (built-in) | Alarm scheduling |
@lumenize/fetch | fetch | Yes | Proxy fetch (cost optimization) |
For debug logging, use the standalone @lumenize/debug package — see Debug. It's not a NADIS service because it works cross-platform (Workers, Node.js, browsers).
Service: sql Template Literal
const users = this.svc.sql`SELECT * FROM users WHERE active = ${true}`;
this.svc.sql`INSERT INTO users (id, email) VALUES (${id}, ${email})`;
Parameters are automatically bound and SQL-injection safe. See SQL Template Literal for complex queries, pagination, and standalone usage.
Service: alarms Scheduling
class MyDO extends LumenizeDO<Env> {
// No alarm() override needed - LumenizeDO handles it automatically!
scheduleTask() {
// Schedule with continuations (type-safe!)
const schedule = this.svc.alarms.schedule(
60, // Seconds from now
this.ctn().handleTask({ userId: '123' })
);
// Cancel if needed
this.svc.alarms.cancelSchedule(schedule.id);
}
// Handler method
handleTask(payload: { userId: string }) {
console.log('Task for user:', payload.userId);
}
}
See Alarms for cron scheduling, recurring tasks, and advanced patterns.
Storage Patterns
Synchronous Storage
Always use synchronous storage APIs:
// ✅ Correct - synchronous KV
this.ctx.storage.kv.put('key', value);
const value = this.ctx.storage.kv.get('key');
this.ctx.storage.kv.delete('key');
// ✅ Correct - this.svc.sql uses synchronous ctx.storage.sql.exec under the covers
const users = this.svc.sql`SELECT * FROM users WHERE active = ${true}`;
// ❌ Wrong - legacy async (don't use)
await this.ctx.storage.put('key', value);
Requires compatibility_date: "2025-09-12" or later in wrangler.jsonc.
Keep Methods Synchronous
Critical rule: Keep mesh handler methods synchronous to take advantage of DO's consistency model (input gates).
// ✅ Correct - synchronous read-modify-write
@mesh()
addSubscriber(userId: string) {
const subscribers = this.ctx.storage.kv.get('subscribers') ?? [];
subscribers.push(userId);
this.ctx.storage.kv.put('subscribers', subscribers);
}
// ❌ Wrong - async/await risks race condition
@mesh()
async addSubscriber(userId: string) {
const subscribers = this.ctx.storage.kv.get('subscribers') ?? []; // ['alice']
await somePromise(); // Input gate opens! Another call adds 'bob' → ['alice', 'bob']
subscribers.push(userId); // Our stale copy: ['alice', 'charlie']
this.ctx.storage.kv.put('subscribers', subscribers); // Overwrites — 'bob' is lost!
}
In the wrong example, the await opens the input gate, allowing another request to modify the array. When we put our stale copy, we overwrite changes made by the other request.
Testing
Test DOs using @lumenize/testing with tunneling — direct access to DO internals from your test. See @lumenize/testing for complete patterns including browser simulation, WebSocket testing, and multi-instance scenarios.
Lifecycle Hooks
LumenizeDO provides lifecycle hooks so subclasses can customize behavior at specific points without overriding the constructor or fetch(). The base class owns the invariants (identity initialization, concurrency control, etc.) and calls your hook at the right moment.
| Hook | When it runs | Sync/Async |
|---|---|---|
onStart() | During construction, before any requests | async (in blockConcurrencyWhile) |
onRequest(request) | On HTTP requests, after identity initialization | sync |
onStart()
Override for initialization — creating tables, loading config, or anything you'd normally do in a constructor. Automatically wrapped in blockConcurrencyWhile, guaranteeing it completes before the DO handles any fetch(), alarm(), RPC calls, etc.
onStart?(): Promise<void>
class MyDO extends LumenizeDO<Env> {
async onStart() {
const res = await fetch('https://cfg.example.com/init');
this.ctx.storage.kv.put('config', await res.json());
}
}
onRequest(request)
Optional synchronous HTTP request handler. Called after identity initialization, so this.lmz.instanceName and this.lmz.bindingName are available. If not implemented, the DO returns 501 for HTTP requests.
onRequest?(request: Request): Response
class StatusDO extends LumenizeDO<Env> {
onRequest(request: Request): Response {
const url = new URL(request.url);
if (url.pathname.endsWith('/who-is-this')) {
return Response.json({ name: this.lmz.instanceName, binding: this.lmz.bindingName });
}
return new Response('Not Found', { status: 404 });
}
}
onRequest — don't override fetch()Overriding fetch() would skip identity initialization and not assure that this.lmz is ready. onRequest is synchronous by design. Use calls for dispatching work from a sync context.
onBeforeCall()
Class-wide access control hook that runs before every incoming mesh call. See Security: Access Control for patterns and examples.
API Reference
See Mesh API for the full LumenizeDO class reference including ctx, env, svc, lmz, onStart(), onBeforeCall(), onRequest(), and ctn<T>().