Patterns
Recipes for common problems: ownership, safe creates, uniqueness, counts, imports and backfills.
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.
links: {
orders_orderOwner: {
forward: { on: "orders_order", has: "one", label: "owner" },
reverse: { on: "$users", has: "many", label: "orders" },
},
},await runtime.tx((tx) =>
tx.orders_order[input.orderId].update({ status: "open" }).link({ owner: runtime.auth.user.id }),
);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:
orders_lineItem: i.entity({
orderId: i.string().indexed(),
sku: i.string().indexed(),
orderSku: i.string().unique().indexed(), // `${orderId}:${sku}`
}),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.lengthWhen 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.
Find objects with no link
db.useQuery({ orders_order: { $: { where: { "owner.id": { $isNull: true } } } } });