Durable Objects -- Extending Real-Time Capabilities

Priority: P1 (High Value)

What are Durable Objects?

Workers with globally unique identity, persistent storage (SQLite or KV), and strong consistency. Each DO instance has:

Current Durable Objects Usage

Company Manager \1 in 3 Workers:

1. Rate Limit Worker

2. Gaming Worker

3. Chat Worker

4. Bookings Worker

New Integration Opportunities

1. Multi-Tenant POS Session Management

\1: POS operations require real-time coordination -- multiple terminals at the same location need consistent state (open tabs, inventory holds, active discounts).

\1: One DO per POS location, managing all terminal sessions.


export class POSSession extends DurableObject {
  private sql: SqlStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;

    // Initialize SQLite tables
    this.sql.exec(`
      CREATE TABLE IF NOT EXISTS open_tabs (
        id TEXT PRIMARY KEY,
        terminal_id TEXT NOT NULL,
        items TEXT NOT NULL DEFAULT '[]',
        created_at INTEGER NOT NULL,
        updated_at INTEGER NOT NULL
      );
      CREATE TABLE IF NOT EXISTS inventory_holds (
        product_id TEXT NOT NULL,
        quantity INTEGER NOT NULL,
        terminal_id TEXT NOT NULL,
        expires_at INTEGER NOT NULL
      );
    `);
  }

  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    switch (url.pathname) {
      case "/ws":
        // WebSocket for real-time terminal updates
        const pair = new WebSocketPair();
        this.ctx.acceptWebSocket(pair[1]);
        return new Response(null, { status: 101, webSocket: pair[0] });

      case "/open-tab":
        return this.openTab(await request.json());

      case "/add-item":
        return this.addItem(await request.json());

      case "/hold-inventory":
        return this.holdInventory(await request.json());

      case "/close-tab":
        return this.closeTab(await request.json());

      default:
        return new Response("Not Found", { status: 404 });
    }
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    const data = JSON.parse(message);
    // Handle real-time terminal coordination
    // Broadcast state changes to all connected terminals
    for (const client of this.ctx.getWebSockets()) {
      if (client !== ws) {
        client.send(JSON.stringify({ type: "state-update", data }));
      }
    }
  }

  // Alarm: clean up expired inventory holds
  async alarm() {
    this.sql.exec(
      "DELETE FROM inventory_holds WHERE expires_at < ?",
      Date.now()
    );
    // Schedule next cleanup in 5 minutes
    this.ctx.storage.setAlarm(Date.now() + 5 * 60 * 1000);
  }
}

\1: \1

2. Collaborative Document Editing

\1: The canvas feature needs real-time collaboration without conflicts.

\1: One DO per document, using operational transform or CRDT.


export class CollaborativeDocument extends DurableObject {
  private sql: SqlStorage;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.sql = ctx.storage.sql;

    this.sql.exec(`
      CREATE TABLE IF NOT EXISTS operations (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id TEXT NOT NULL,
        op_type TEXT NOT NULL,
        op_data TEXT NOT NULL,
        timestamp INTEGER NOT NULL
      );
      CREATE TABLE IF NOT EXISTS presence (
        user_id TEXT PRIMARY KEY,
        cursor_pos TEXT,
        selection TEXT,
        last_seen INTEGER
      );
    `);
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    const { type, userId, data } = JSON.parse(message);

    switch (type) {
      case "operation":
        // Store operation
        this.sql.exec(
          "INSERT INTO operations (user_id, op_type, op_data, timestamp) VALUES (?, ?, ?, ?)",
          userId, data.type, JSON.stringify(data), Date.now()
        );
        // Broadcast to other editors
        this.broadcast(ws, { type: "remote-operation", userId, data });
        break;

      case "presence":
        // Update cursor/selection
        this.sql.exec(
          "INSERT OR REPLACE INTO presence (user_id, cursor_pos, selection, last_seen) VALUES (?, ?, ?, ?)",
          userId, JSON.stringify(data.cursor), JSON.stringify(data.selection), Date.now()
        );
        this.broadcast(ws, { type: "remote-presence", userId, data });
        break;
    }
  }

  private broadcast(sender: WebSocket, message: object) {
    const json = JSON.stringify(message);
    for (const ws of this.ctx.getWebSockets()) {
      if (ws !== sender && ws.readyState === WebSocket.OPEN) {
        ws.send(json);
      }
    }
  }
}

\1: \1

3. Real-Time Notifications Hub

\1: No cross-instance notification delivery. Users on different server instances don't get real-time updates.

\1: One DO per user for notification delivery.


export class NotificationHub extends DurableObject {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/ws") {
      const pair = new WebSocketPair();
      this.ctx.acceptWebSocket(pair[1]);
      // Send any queued notifications
      const queued = this.sql.exec("SELECT * FROM pending_notifications ORDER BY created_at").toArray();
      for (const notif of queued) {
        pair[1].send(JSON.stringify(notif));
      }
      // Clear delivered
      this.sql.exec("DELETE FROM pending_notifications");
      return new Response(null, { status: 101, webSocket: pair[0] });
    }

    if (url.pathname === "/notify" && request.method === "POST") {
      const notification = await request.json();
      const clients = this.ctx.getWebSockets();

      if (clients.length > 0) {
        // User is online -- deliver immediately
        for (const ws of clients) {
          ws.send(JSON.stringify(notification));
        }
      } else {
        // User is offline -- queue for later
        this.sql.exec(
          "INSERT INTO pending_notifications (type, data, created_at) VALUES (?, ?, ?)",
          notification.type, JSON.stringify(notification.data), Date.now()
        );
      }

      return new Response("OK");
    }

    return new Response("Not Found", { status: 404 });
  }

  // WebSocket hibernation -- DO sleeps when no messages
  async webSocketClose(ws: WebSocket) {
    // User disconnected, future notifications will be queued
  }
}

\1: \1

4. Workflow State Machine

\1: AI autonomy decisions need state coordination across distributed systems.

\1: One DO per active workflow/decision, tracking state transitions.


export class WorkflowState extends DurableObject {
  async processDecision(decision: AIDecision): Promise<void> {
    const current = this.sql.exec(
      "SELECT * FROM workflow_state WHERE decision_id = ?",
      decision.id
    ).one();

    // State machine transitions
    switch (current?.status) {
      case "PENDING":
        if (decision.action === "approve") {
          await this.transitionTo(decision.id, "APPROVED");
          await this.scheduleExecution(decision);
        }
        break;
      case "APPROVED":
        await this.transitionTo(decision.id, "EXECUTING");
        // Execute via Queue
        break;
      case "EXECUTING":
        await this.transitionTo(decision.id, "EXECUTED");
        // Start rollback window timer
        this.ctx.storage.setAlarm(Date.now() + 24 * 60 * 60 * 1000);
        break;
    }
  }

  async alarm() {
    // Rollback window expired -- finalize
    this.sql.exec(
      "UPDATE workflow_state SET status = 'FINALIZED' WHERE status = 'EXECUTED'"
    );
  }
}

5. Floor Management / Table Ordering

\1: Restaurant floor management needs real-time table status and order coordination.

\1: One DO per restaurant floor.


export class FloorManager extends DurableObject {
  // Tracks: table status, active orders, waiter assignments
  // WebSocket: real-time floor view for all staff tablets
  // Alarms: auto-close idle tables after timeout
}

\1: \1

6. Auction / Bidding System

For classified ads or e-commerce auctions:


export class AuctionRoom extends DurableObject {
  // Strong consistency for bid ordering
  // WebSocket for real-time bid updates
  // Alarm for auction end time
  // SQLite for bid history
}

Hibernatable WebSockets

Key optimization: DOs can hibernate (release memory) while maintaining WebSocket connections. The DO wakes only when a message arrives.


// Instead of this (always in memory):
this.websockets = new Set();
// Use this (hibernatable):
this.ctx.acceptWebSocket(ws);
this.ctx.getWebSockets(); // wakes DO only when needed

\1: You pay $0 for hibernated DOs. Only active compute is billed.

Multi-Tenant DO Naming Convention


{purpose}:{tenantId}:{entityId}

Examples:
pos-session:0195134f-8258-7e85:location-1
notifications:0195134f-8258-7e85:user-abc
floor:0195134f-8258-7e85:loc-1:floor-main
document:0195134f-8258-7e85:doc-xyz
auction:0195134f-8258-7e85:listing-123

Tenant isolation is guaranteed by the naming scheme -- DOs with different names are completely independent.

Limits

MetricFreePaid
SQLite storage/DO5 GB total10 GB per DO
Requests/second/DO1,000 (soft)1,000 (soft)
CPU time/invocation30s (default)Configurable
WebSocket msg size32 MiB32 MiB
Objects/namespaceUnlimitedUnlimited
Namespaces/accountUnlimitedUnlimited

Pricing

MetricIncludedOverage
Requests1M/month$0.15/million
Duration400K GB-s$12.50/M GB-s
SQLite storage5 GB$0.20/GB-month
Hibernated$0$0

Estimated Impact