@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 shardROOT_DO_SIZE_WARN_BYTES: soft cap above which the root DO logs a warningSESSION_DO_TTL_DEFAULT: default session TTL (SessionDO)