# Durable Objects

> Build RPC-style Durable Objects with auto-discovery

Package: Koze
Canonical: https://kuratchi.dev/docs/koze/durable-objects
Markdown: https://kuratchi.dev/docs/koze/durable-objects.md

## Durable Objects

Koze auto-discovers `.do.ts` files in `src/server/`. Public methods become RPC-accessible automatically.

```ts
// src/server/user.do.ts
import { DurableObject } from 'cloudflare:workers';

export default class UserDO extends DurableObject {
  async getName() {
    return await this.ctx.storage.get('name');
  }

  async setName(name: string) {
    if (!name) throw new Error('Name required');
    await this.ctx.storage.put('name', name);
  }

  // Underscore-prefixed methods are not exposed
  _validate() {}
}
```

## Naming Convention

Binding names are derived from the filename:

| File | Binding |
|------|---------|
| `user.do.ts` | `USER_DO` |
| `org-settings.do.ts` | `ORG_SETTINGS_DO` |
| `chat-room.do.ts` | `CHAT_ROOM_DO` |

## RPC Rules

- Public methods are callable through generated proxies
- Underscore-prefixed methods (`_validate`, `_internal`) are not exposed
- Lifecycle methods like `fetch()` and `alarm()` are not treated as RPC methods

## Route Usage

Import directly from the `.do.ts` file using the `$server/` alias:

```html
<script>
  import { getName, setName } from '$server/user.do';
  const name = await getName();
</script>

<h1>Hello, {name}</h1>

<form action={setName} method="POST">
  <input type="text" name="name" required />
  <button type="submit">Update</button>
</form>
```

## Validating RPC Input

Attach schemas by method name with `static schemas`. Koze validates input before invoking the method.

```ts
import { DurableObject } from 'cloudflare:workers';
import { schema, type InferSchema } from '@kuratchi/koze';

export default class UserDO extends DurableObject {
  static schemas = {
    setProfile: schema({
      name: schema.string().min(1),
      likesDogs: schema.boolean().optional(false),
    }),
  };

  async setProfile(data: InferSchema<(typeof UserDO.schemas).setProfile>) {
    await this.ctx.storage.put('profile', data);
  }
}
```

**Rules:**
- Schema-backed RPC methods accept one object argument
- `static schemas` keys must match the public method names exactly
- Invalid payloads return `400`
- Use `InferSchema<(typeof MyDO.schemas).methodName>` for typed parameters

## Schema Examples

**With defaults and arrays:**
```ts
import { DurableObject } from 'cloudflare:workers';
import { schema, type InferSchema } from '@kuratchi/koze';

export default class UserDO extends DurableObject {
  static schemas = {
    savePreferences: schema({
      tags: schema.string().list(),
      marketingEmails: schema.boolean().optional(false),
    }),
  };

  async savePreferences(data: InferSchema<(typeof UserDO.schemas).savePreferences>) {
    await this.ctx.storage.put('preferences', data);
    return { ok: true };
  }
}
```

## Stub Resolution

By default, the framework's auto-discovered DOs resolve via `idFromName('global')` — every binding points to a singleton instance per worker. Apps that need per-user / per-tenant routing register a custom resolver at runtime:

```ts
// src/server/do-routing.ts
import { __registerDoResolver } from '@kuratchi/koze/runtime/do.js';
import { getCurrentUser } from '@kuratchi/kyzen';
import { env } from 'cloudflare:workers';

__registerDoResolver('USER_DO', async () => {
  const user = await getCurrentUser();
  if (!user?.organizationId) return null;
  const ns = (env as any).USER_DO;
  return ns.get(ns.idFromName(user.organizationId));
});
```

Import the routing module from `src/middleware.ts` (any side-effect import works) so the resolver registers before the first request:

```ts
import './server/do-routing';
```

For multi-tenant org databases specifically, `kyzen` ships `getOrgClient(organizationId)` (resolves the routing key from the admin DB then returns a stub) and `getOrgStubByName(doName)` (sync; pass the routing key directly). The auth package uses `getOrgStubByName` internally during signin/signup, so apps usually just call `getCurrentUser()` and let the package handle routing — see [Organizations and Schema](/docs/kyzen/organizations-and-schema) for the full pattern.

## Storage API

Use the Durable Object storage API for persistence:

```ts
export default class CounterDO extends DurableObject {
  async increment() {
    const current = (await this.ctx.storage.get('count')) || 0;
    await this.ctx.storage.put('count', current + 1);
    return current + 1;
  }

  async getCount() {
    return (await this.ctx.storage.get('count')) || 0;
  }

  async reset() {
    await this.ctx.storage.delete('count');
  }
}
```

## Alarms

Schedule future work with alarms:

```ts
export default class ReminderDO extends DurableObject {
  async scheduleReminder(delayMs: number) {
    const alarmTime = Date.now() + delayMs;
    await this.ctx.storage.setAlarm(alarmTime);
    return { scheduled: new Date(alarmTime).toISOString() };
  }

  async alarm() {
    // Called when the alarm fires
    await this.sendReminder();
  }
}
```

## WebSockets

Handle WebSocket connections in Durable Objects:

```ts
export default class ChatRoomDO extends DurableObject {
  async fetch(request: Request) {
    const upgradeHeader = request.headers.get('Upgrade');
    
    if (upgradeHeader === 'websocket') {
      const pair = new WebSocketPair();
      const [client, server] = Object.values(pair);
      
      this.ctx.acceptWebSocket(server);
      
      return new Response(null, {
        status: 101,
        webSocket: client,
      });
    }
    
    return new Response('Expected WebSocket', { status: 400 });
  }

  async webSocketMessage(ws: WebSocket, message: string) {
    // Broadcast to all connected clients
    for (const client of this.ctx.getWebSockets()) {
      client.send(message);
    }
  }
}
```

## Wrangler Configuration

The framework auto-syncs Durable Object bindings to `wrangler.jsonc`:

```jsonc
{
  "durable_objects": {
    "bindings": [
      {
        "name": "USER_DO",
        "class_name": "UserDO"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["UserDO"]
    }
  ]
}
```
