# Patterns

URL: https://docs.domainruntime.dev/docs/data/patterns
Status: Guide
Reviewed: 2026-09-23



<div className="docs-note">
  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](/docs/data/actions)

  .
</div>

## Give every object an owner [#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.

```ts title="schema"
links: {
  orders_orderOwner: {
    forward: { on: "orders_order", has: "one", label: "owner" },
    reverse: { on: "$users", has: "many", label: "orders" },
  },
},
```

```ts title="action"
await runtime.tx((tx) =>
  tx.orders_order[input.orderId].update({ status: "open" }).link({ owner: runtime.auth.user.id }),
);
```

```ts title="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 [#make-creates-safe-to-retry]

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

```ts
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 [#unique-across-two-fields]

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

```ts title="schema"
orders_lineItem: i.entity({
  orderId: i.string().indexed(),
  sku: i.string().indexed(),
  orderSku: i.string().unique().indexed(),   // `${orderId}:${sku}`
}),
```

```ts title="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 [#do-not-check-then-write-for-uniqueness]

```ts
// ❌ 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 [#keep-a-count]

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

```ts
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](/docs/data/workflows) step instead of read-modify-write in parallel actions.

## Import thousands of rows [#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:

```ts
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 [#backfill-an-attribute]

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

```ts
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 [#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.

## Find objects with no link [#find-objects-with-no-link]

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