HighX
Server

Mutations

Writes. One transaction each, and how to refuse a stale one.

A mutation writes tenant data. The handler runs inside one transaction: throw, and nothing is written — there is no partial state to clean up.

import { mutation } from "@highx/core";

export const saveNote = mutation({
  name: "notes.save",
  args: noteArgs,
  returns: noteId,
  auth: can("notes:write"),
  handler: (ctx, args) => ctx.db.table("notes").insert({
    ...args,
    authorId: ctx.identity.userId,
    createdAt: ctx.now(),
  }),
});

What the handler returns is what the caller receives — over HTTP and over the socket alike. An inserted id, a new version, whatever is useful.

Row operations

const id = ctx.db.table("notes").insert({ title: "New" });
const note = ctx.db.table("notes").get(id);
ctx.db.table("notes").update(id, { title: "Renamed" });
ctx.db.table("notes").delete(id);
ctx.db.table("notes").list();          // every row — see Queries on scans

No network I/O

A mutation cannot fetch. That restriction is what lets it be a transaction at all — a transaction that awaits a third party holds storage open for as long as that party takes. Reaching outside is an action's job.

Writes are serialized per tenant

A tenant's data lives in one Durable Object, and a Durable Object is single-threaded. Two mutations to the same tenant therefore run one after the other, and a read-modify-write inside a handler cannot have another write land in the middle of it.

That is what makes the next pattern a genuine compare-and-swap rather than an approximation.

Refusing a stale write

A shared record should take the version the edit was based on, and refuse a write built on a version it has already moved past:

const existing = ctx.db.table("notes").get(args.id);
if (existing === undefined) throw new HighXError("not_found", "That note is gone");
if (args.baseUpdatedAt !== existing["updatedAt"]) {
  throw new HighXError("conflict", `${existing["editedBy"]} changed this first`);
}
ctx.db.table("notes").update(args.id, { ...args.changes, updatedAt: ctx.now() });

Six lines, and they are the difference between collaborative and lossy. A client that was offline replays its queued writes on reconnect — idempotently, under the same request ID — and idempotent is not the same as safe: without this check, that replay lands on top of everything written while it was away. The write succeeds, the work is gone, and nothing reports a problem.

Make the version required, not optional. An optional guard is not a guard — an old tab, a script, or a forgotten field silently gets unconditional overwrite, which is the exact behaviour it exists to prevent.

This is the right shape for a record. A field several people edit at once wants a collaborative field instead, where the merge is the point and a refusal would be the failure.

On the client, useDraft turns the refusal into a choice the user can answer.

Errors a client should act on

Throw HighXError(code, message) for any outcome the interface has to explain. A plain Error is reported as "an unexpected error occurred" — right for a bug, wrong for a conflict.

Identity, never arguments

// Wrong — a client can send any tenantId it likes.
handler: (ctx, args) => ctx.db.query("notes").withIndex("byTenant", { tenantId: args.tenantId })

// Right — the session decided this.
handler: (ctx) => ctx.db.query("notes").withIndex("byTenant", { tenantId: ctx.identity.tenantId })

A well-formed tenant ID from an attacker is still an attacker's tenant ID.

Next

On this page