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

Patterns

Recipes for common problems: ownership, safe creates, uniqueness, counts, imports and backfills.

The recipes use the planned runtime.tx and runtime.query. The same patterns run today with await runtime.db(); runtime.auth works as shown. See Actions.

Give every object an owner

Link the object to the caller in the same tx that creates it, and write the rule against the link.

schema
links: {
  orders_orderOwner: {
    forward: { on: "orders_order", has: "one", label: "owner" },
    reverse: { on: "$users", has: "many", label: "orders" },
  },
},
action
await runtime.tx((tx) =>
  tx.orders_order[input.orderId].update({ status: "open" }).link({ owner: runtime.auth.user.id }),
);
permissions
orders_order: {
  allow: {
    view: "auth.id in data.ref('owner.id')",
    create: "auth.id in data.ref('owner.id')",
  },
},

The create rule sees the object after the write, links included. An action that forgets .link({ owner }) is rejected, so the bug is caught instead of shipped.

Make creates safe to retry

Let the caller choose the id, and treat a repeat as a lookup:

input: z.object({ itemId: z.string().uuid(), label: z.string().min(1) }),
async execute({ input, runtime }) {
  const { items_item } = await runtime.query({ items_item: { $: { where: { id: input.itemId } } } });
  if (items_item[0]) return { itemId: input.itemId };
  await runtime.tx((tx) => tx.items_item[input.itemId].update({ label: input.label }));
  return { itemId: input.itemId };
}

Two concurrent repeats write the same object with the same values, so the race is harmless.

Unique across two fields

There are no composite keys. Store the combination in a unique attribute and set it in the action:

schema
orders_lineItem: i.entity({
  orderId: i.string().indexed(),
  sku: i.string().indexed(),
  orderSku: i.string().unique().indexed(),   // `${orderId}:${sku}`
}),
action
const key = `${input.orderId}:${input.sku}`;
tx.orders_lineItem[lookup("orderSku", key)].update({
  orderId: input.orderId, sku: input.sku, orderSku: key, quantity: input.quantity,
});

Because actions are the only writers, orderSku never drifts from orderId and sku.

Do not check-then-write for uniqueness

// ❌ two fast calls both see "no duplicate" and both create
const { orders_customer } = await runtime.query({ orders_customer: { $: { where: { email } } } });
if (orders_customer.length) throw new Error("exists");
await runtime.tx((tx) => tx.orders_customer[id()].update({ email }));

Let the database enforce it: mark email .unique() and write with lookup("email", email). A duplicate either updates the same object or fails the tx with record-not-unique.

Keep a count

Do not store a counter that two actions increment at the same time. Count the linked objects:

db.useQuery({ orders_customer: { $: { where: { id } }, orders: { $: { fields: ["id"] } } } });
// data.orders_customer[0].orders.length

When the list is too large to load, recompute the number in a workflow step instead of read-modify-write in parallel actions.

Import thousands of rows

Keep one tx small — hundreds of operations, not thousands. Split large imports into a workflow with one step per batch, so a crash resumes from the last batch:

export async function importOrders({ input, runtime }) {
  "use workflow";
  for (const batch of chunk(input.rows, 200)) {
    await writeBatch(runtime, batch);
  }
  return { imported: input.rows.length };
}

async function writeBatch(runtime, rows) {
  "use step";
  await runtime.tx((tx) => rows.map((r) => tx.orders_order[r.id].update(r)));
}

Use ids from the source rows so a retried batch overwrites instead of duplicating.

Backfill an attribute

To start requiring a value that old objects lack, fill it first with a one-off action meant for your backend:

backfillStatus: defineAction({
  input: z.object({}),
  output: z.object({ updated: z.number() }),
  async execute({ runtime }) {
    if (!runtime.auth.admin) throw new Error("Service key only");
    const { orders_order } = await runtime.query({
      orders_order: { $: { where: { status: { $isNull: true } }, fields: ["id"] } },
    });
    await runtime.tx((tx) => orders_order.map((o) => tx.orders_order[o.id].update({ status: "open" })));
    return { updated: orders_order.length };
  },
}),

Seed a development environment

Nothing is copied between environments. Write a seed action that creates realistic data with fixed ids, so running it twice is harmless, and run it after creating the environment. Never point development code at production data.

db.useQuery({ orders_order: { $: { where: { "owner.id": { $isNull: true } } } } });

On this page