state-schema
coreDefining typed state schemas for @durable-streams/state. createStateSchema() with CollectionDefinition (schema, type, primaryKey), Standard Schema validators (Zod, Valibot, ArkType), event helpers insert/update/delete/upsert, ChangeEvent and ControlEvent types, State Protocol operations, transaction IDs (txid) for write confirmation. Load when defining entity types, choosing a schema validator, or creating typed change events.
Durable Streams — State Schema
Define typed entity collections over durable streams using Standard Schema validators. Schemas route stream events to collections, validate data, and provide typed helpers for creating change events.
Setup
import { createStateSchema } from "@durable-streams/state"
import { z } from "zod" // Use the correct import for your Zod version (e.g. "zod/v4" for Zod v4)
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
const messageSchema = z.object({
id: z.string(),
text: z.string(),
userId: z.string(),
timestamp: z.number(),
})
const schema = createStateSchema({
users: {
schema: userSchema,
type: "user", // Event type field — routes events to this collection
primaryKey: "id", // Field in value used as unique key
},
messages: {
schema: messageSchema,
type: "message",
primaryKey: "id",
},
})
Core Patterns
Creating typed change events
Schema collections provide typed helpers for building events:
// Insert
const insertEvent = schema.users.insert({
value: { id: "1", name: "Kyle", email: "kyle@example.com" },
})
// Update
const updateEvent = schema.users.update({
value: { id: "1", name: "Kyle Mathews", email: "kyle@example.com" },
oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" },
})
// Delete
const deleteEvent = schema.users.delete({
key: "1",
oldValue: { id: "1", name: "Kyle", email: "kyle@example.com" },
})
Using transaction IDs for confirmation
const txid = crypto.randomUUID()
const event = schema.users.insert({
value: { id: "1", name: "Kyle", email: "kyle@example.com" },
headers: { txid },
})
await stream.append(event)
// Then use db.utils.awaitTxId(txid) in StreamDB for confirmation
Choosing a schema validator
Any library implementing Standard Schema works:
// Zod
import { z } from "zod"
const userSchema = z.object({ id: z.string(), name: z.string() })
// Valibot
import * as v from "valibot"
const userSchema = v.object({ id: v.string(), name: v.string() })
// Manual Standard Schema implementation
const userSchema = {
"~standard": {
version: 1,
vendor: "my-app",
validate: (value) => {
if (typeof value === "object" && value !== null && "id" in value) {
return { value }
}
return { issues: [{ message: "Invalid user" }] }
},
},
}
Event types and type guards
import { isChangeEvent, isControlEvent } from "@durable-streams/state"
import type {
StateEvent,
ChangeEvent,
ControlEvent,
} from "@durable-streams/state"
function handleEvent(event: StateEvent) {
if (isChangeEvent(event)) {
// event.type, event.key, event.value, event.headers.operation
console.log(`${event.headers.operation}: ${event.type}/${event.key}`)
}
if (isControlEvent(event)) {
// event.headers.control: "snapshot-start" | "snapshot-end" | "reset"
console.log(`Control: ${event.headers.control}`)
}
}
Common Mistakes
CRITICAL Using primitive values instead of objects in collections
Wrong:
{ type: "count", key: "views", value: 42 }
Correct:
{ type: "count", key: "views", value: { count: 42 } }
Collections require object values so the primaryKey field can be extracted. Primitive values throw during dispatch.
Source: packages/state/README.md best practices
HIGH Using duplicate event types across collections
Wrong:
createStateSchema({
users: { schema: userSchema, type: "entity", primaryKey: "id" },
posts: { schema: postSchema, type: "entity", primaryKey: "id" },
})
Correct:
createStateSchema({
users: { schema: userSchema, type: "user", primaryKey: "id" },
posts: { schema: postSchema, type: "post", primaryKey: "id" },
})
createStateSchema() throws if two collections share the same type string. The type field routes events to collections — duplicates would be ambiguous.
Source: packages/state/src/stream-db.ts createStateSchema validation
HIGH Forgetting to use a Standard Schema-compatible validator
Wrong:
interface User {
id: string
name: string
}
createStateSchema({
users: { schema: User, type: "user", primaryKey: "id" }, // Not a validator!
})
Correct:
import { z } from "zod"
const userSchema = z.object({ id: z.string(), name: z.string() })
createStateSchema({
users: { schema: userSchema, type: "user", primaryKey: "id" },
})
The schema field requires an object implementing the ~standard interface. TypeScript interfaces and plain types are not validators.
Source: packages/state/README.md Standard Schema support section
MEDIUM Using reserved collection names
Wrong:
createStateSchema({
actions: { schema: actionSchema, type: "action", primaryKey: "id" },
})
Correct:
createStateSchema({
userActions: { schema: actionSchema, type: "action", primaryKey: "id" },
})
Collection names collections, preload, close, utils, and actions are reserved — they collide with the StreamDB API surface.
Source: packages/state/src/stream-db.ts reserved name check
HIGH Tension: Schema strictness vs. prototyping speed
This skill's patterns conflict with getting-started. The state package requires Standard Schema validators and typed collections, while quick prototyping favors raw JSON streams without schemas. Agents may jump to StreamDB for a simple demo when raw stream() with JSON mode would be faster.
See also: durable-streams/getting-started/SKILL.md
See also
- stream-db — Wire schemas into a reactive StreamDB
Version
Targets @durable-streams/state v0.2.1.