DomainRuntimedocs
Working with data
VerifiedWhat works todayReviewed 2026-09-23

Modeling data

Entities, attributes, links and indexes, declared in your domain.

You describe your data in the domain with withSchema. Entities are collections of objects; links connect them.

src/domain.ts
import { domain, i } from "@domainruntime/domain";

export default domain("orders").withSchema({
  entities: {
    orders_customer: i.entity({
      name: i.string(),
      email: i.string().unique().indexed(),
    }),
    orders_order: i.entity({
      reference: i.string().unique().indexed(),
      status: i.string().indexed(),
      total: i.number(),
      placedAt: i.date().indexed(),
      notes: i.string().optional(),
    }),
  },
  links: {
    orders_orderCustomer: {
      forward: { on: "orders_order", has: "one", label: "customer" },
      reverse: { on: "orders_customer", has: "many", label: "orders" },
    },
  },
  rooms: {},
});

entities, links and rooms are all required today, even when empty.

Attributes

TypeStores
i.string()text
i.number()numbers
i.boolean()true / false
i.date()a point in time
i.json()any JSON value, stored as-is

Attributes are required unless you add .optional(). Every entity also has an id.

ModifierEffect
.optional()the value may be missing
.unique()no two objects share the value; a write that would duplicate it fails with record-not-unique
.indexed()needed to filter with comparisons ($gt, $like…) and to order by it

Index what you filter and sort on. Equality filters work without an index but get slower as the entity grows.

A link has two directions, each with a label and a cardinality:

orders_orderCustomer: {
  forward: { on: "orders_order", has: "one", label: "customer" },
  reverse: { on: "orders_customer", has: "many", label: "orders" },
}

An order now has customer and a customer has orders, and you can query either way. Many-to-many is has: "many" on both sides:

orders_orderTags: {
  forward: { on: "orders_order", has: "many", label: "tags" },
  reverse: { on: "orders_tag", has: "many", label: "orders" },
},

Every label must be unique on its entity: two links that both add an orders label to orders_customer collide. Name them by role (placedOrders, approvedOrders).

Add onDelete: "cascade" to the side that should disappear with its parent:

forward: { on: "orders_lineItem", has: "one", label: "order", onDelete: "cascade" },

Composing domains

Large products are several small domains. A domain can include another and link to its entities:

import orders from "./orders";

export default domain("billing")
  .includes(orders)
  .withSchema({
    entities: {
      billing_invoice: i.entity({ number: i.string().unique().indexed(), amount: i.number() }),
    },
    links: {
      billing_invoiceOrder: {
        forward: { on: "billing_invoice", has: "one", label: "order" },
        reverse: { on: "orders_order", has: "many", label: "invoices" },
      },
    },
    rooms: {},
  });

Including a domain brings its entities into scope. It does not expose its actions: each domain decides which actions its callers can run. See Actions.

Why the prefix. Entities share one namespace per environment and domains are composed, so orders_order and billing_order can live side by side, and anyone reading a query knows which domain owns each entity.

Changing the schema

Publishing applies your schema to the environment's database, the same way in development and in production. Every change is checked against the data already stored before anything is written: either the whole schema applies, or nothing does and the previous release keeps serving.

ChangeWhat happens
New entity, attribute or linkapplied
New index, or index removedapplied
.unique() addedapplied if the stored values are unique; otherwise refused, listing the duplicates
.unique() removed, attribute made optionalapplied
Type changedapplied if every stored value fits the new type; otherwise refused with a count and samples
Attribute made requiredapplied if every object has a value; otherwise refused
New required attribute on an entity that already has objectsrefused: add it with .optional() first, fill it, then make it required
Link cardinality narrowed (many → one)applied if the data fits; otherwise refused
Entity, attribute or link removedrefused until you confirm it; see below

A refusal answers 409 schema_change_refused with one entry per problem, so the CLI can show each one:

{ "error": "schema_change_refused",
  "details": { "issues": [
    { "where": "orders_order.reference", "code": "duplicates", "message": "unique has 1 duplicate value (A-1 ×2)" }
  ] } }

Each publish reports what it did, e.g. +2 attrs, +1 index, +1 link, or no changes.

Removing and renaming

Removing something from the schema is destructive, so it needs a confirmation, as in InstantDB's instant-cli push:

druntime push --yes                          # confirm every removal of something your domain declared
druntime push --delete orders_order.notes    # confirm one removal by name
druntime push --rename orders_order.ref:orders_order.reference   # same attribute, new name, data kept

Without a confirmation, the publish is refused with 409 removal_unconfirmed, listing what would be deleted. Without --rename, a rename is a removal plus a new attribute.

A confirmed removal is a soft delete: the attribute and its data are hidden, not erased, for 2 days. Within that time you can bring them back:

druntime schema deleted                          # what was removed, and until when it can come back
druntime schema restore orders_order.notes       # back, with its data (not indexed, not required)

After 2 days the data is purged for good.

Something in the database that no release ever declared (written directly, or left from an earlier setup) is kept and reported as a warning on every publish. It is deleted only when you name it with --delete; --yes never includes it.

System entities

Every environment has these. You can link to them and query them:

EntityHoldsDefault visibility
$usersthe users of your app. See Users.each user sees their own row
$filesuploaded files. See Files.hidden until your rules allow it
$streamsstreams. See Streams.hidden until your rules allow it
$executionsaction runseach user sees the runs they started

Link your entities to $users to model ownership; Permissions shows how rules use it.

On this page