Skip to content
DocspackagesDocumentation

@lunora/do

The Durable Object base classes — ShardDO and SessionDO.

PackagesDo

@lunora/do ships the Durable Object base classes. ShardDO is the one the runtime drives on every request; SessionDO is a standalone building block (see the caveat below). You only touch them in src/server/*.ts when you need to add a custom RPC method or wire bespoke storage logic. The defaults are enough for most apps.

ShardDO

Base class for every shard. Owns the per-shard SQLite storage (one DB per DO instance), broadcasts subscription deltas to connected WebSockets, and implements the RPC fan-in.

import { ShardDO } from "lunorash/do";

export class MyShardDO extends ShardDO {
    // Optional: extend with custom RPC methods.
    public async customMethod(input: { foo: string }): Promise<{ ok: true }> {
        return { ok: true };
    }
}

The root shard (when you haven't called .shardBy(...) yet) routes to the DO named ROOT_SHARD_NAME. Storage above ROOT_DO_SIZE_WARN_BYTES emits a console warning so you remember to call .shardBy() before the DO hits its single-instance ceiling.

ShardDOOptions

Per-shard configuration. The generated shard passes these through to super(state, env, options) from the object you hand createShardDO(...), so you set them there rather than subclassing.

Prefer the generated app builder where you have one — defineApp() carries the same knobs as chainable methods and wires the worker half of each capability at the same time:

// src/server/index.ts
import { defineApp } from "../../lunora/_generated/app";

const app = defineApp<Env>()
    .shard((env) => env.SHARD)
    // `true` takes the defaults; an object tunes the caps.
    .reactiveCache({ maxBytes: 4 * 1024 * 1024, maxEntries: 1000 })
    .build();

export const ShardDO = app.ShardDO;

Hand-composing the worker instead, the same options are the createShardDO config:

export const ShardDO = createShardDO({
    // Memoize query results per shard, keyed by caller identity + args.
    // `true` takes the defaults; an object tunes the caps. Omit to disable.
    reactiveCache: { maxBytes: 4 * 1024 * 1024, maxEntries: 1000 },
    // Ceiling on the join keys one relation-crossing `where` may pre-resolve
    // before failing closed. Raise it for a trusted large fan-in relation.
    maxRelationKeys: 5000,
    // How a relation-crossing `where` resolves against a co-located child:
    // "auto" (cost-based, the default), "always" (inline EXISTS), or
    // "never" (universal semijoin). All three return identical rows.
    relationExistsPushDown: "auto",
});

reactiveCache is per-shard and in-memory: it is lost on DO restart and on hibernation, so a cold shard simply re-runs the query. Only registered query functions are memoized — a mutation or action always runs. Live counters (hits, misses, evictions, entries, bytes) show up on the studio's metrics panel.

SessionDO

A TTL'd session store in a Durable Object: records expire after SESSION_DO_TTL_DEFAULT seconds (7 days) unless a shorter TTL is requested, a GC alarm sweeps daily, and instance count stays bounded by keying on the token prefix (idFromName(token.slice(0, 16))). Every request must present the SESSION_DO_SECRET shared secret in the x-lunora-session-do-secret header; when the secret is unset the DO rejects all calls with 401.

@lunora/auth does not use this class. It never has: grep -rn "SessionDO" packages/auth/src/ finds nothing, and no commit in that package's history references it. So exporting SessionDO, binding it as SESSION, and setting SESSION_DO_SECRET gets you a correctly-configured object that nothing ever calls; sessions still live in the auth database.

For auth on Durable Object storage, use LunoraAuthDO / .auth({ namespace }) from @lunora/auth. That path puts the whole better-auth schema, sessions included, in an object with real transactions (which is what @better-auth/scim requires). SessionDO remains usable on its own terms as a TTL'd token store; it is just not wired into auth.

import { SessionDO } from "lunorash/do";

export class MySessionDO extends SessionDO {}

SessionRecord

Type-only export. The shape of an entry in the session DO's storage:

interface SessionRecord {
    userId: string;
    createdAt: number;
    expiresAt: number;
}

Hibernation

ShardDO runs with WebSocket hibernation enabled: inactive sockets cost zero CPU until a message arrives. Exposed via the HibernatableWebSocket type for unit tests. (SessionDO has no WebSocket surface at all: it is HTTP-only, so hibernation does not apply to it.)

ShardDOState / MutationDelta

Internal serialization shapes. Stable across patch releases so add-ons can type their broadcast payloads.

SocketAttachment, RpcRequest, SubscriptionEnvelope and SubscriptionQuery are not exported from here — they belong to the host-neutral engine, so import them from @lunora/shard-engine.

Constants

  • ROOT_SHARD_NAME: the DO instance name used by the root shard
  • ROOT_DO_SIZE_WARN_BYTES: soft cap above which the root DO logs a warning
  • SESSION_DO_TTL_DEFAULT: default session TTL (SessionDO)