Skip to content

feat: Mutator example#2

Draft
Chriztiaan wants to merge 2 commits into
feat/initfrom
feat/mutator
Draft

feat: Mutator example#2
Chriztiaan wants to merge 2 commits into
feat/initfrom
feat/mutator

Conversation

@Chriztiaan
Copy link
Copy Markdown
Collaborator

@Chriztiaan Chriztiaan commented May 7, 2026

This PoC explores the idea of a mutator pattern for use in the write path. A strong example to reference is Zero's implementation.

The client defines a named mutator with an args schema and a run function that writes to the local SQLite database, including metadata about the mutator call (the mutator name and its arguments), and is invoked directly from the UI: mutate.listCreate({ id, name }).
This write is queued via the ps_crud upload queue, where a matching server-side mutator with the same name receives the original args and applies them directly to the source database, with no CRUD translation needed.

// 1. Call in UI
const createNewList = async (name: string) => {
    await mutate.listCreate({ id: uuid(), name });
};

// 2. Client mutator definition
// the run callback writes against the powersync db and applies the needed metadata
import { z } from 'zod';

const listCreateArgs = z.object({
  id: z.string().uuid(),
  name: z.string().min(1)
});

const listCreate: ClientMutator<typeof listCreateArgs> = {
  args: listCreateArgs,
  run: async (args, tx, ctx) => {
    await tx.execute(
      `INSERT INTO ${LISTS_TABLE} (id, created_at, name, owner_id, _metadata)
       VALUES (?, datetime(), ?, ?, ?)`,
      [args.id, args.name, ctx.userId, ctx.metadata]
    );
  }
};

// 3. Backend mutator definition
// The run function can take the received args and apply it to the source db directly
const listCreate: ServerMutator<typeof listCreateArgs> = {
  args: listCreateArgs,
  run: async (args, ctx) => {
    await ctx.pg.query(
      `INSERT INTO lists (id, name, owner_id) VALUES ($1, $2, $3)`,
      [args.id, args.name, ctx.userId]
    );
  }
};

Pros:

  • This means no CRUD-to-SQL translation on the backend. The server works with typed, named arguments rather than raw operations
  • Business logic lives in a well-defined, co-located, testable place on both client and server
  • Potentially a better fit for complex or enterprise projects where fine-grained control over each write operation matters
  • Opens the door to richer validation (via Zod schemas shared between client and server)

What's still open: Delete workaround, metadata threading made easier

AI disclosure

This PR was created with the help of Claude Code. Help constitutes assistance in research, planning, and rough outline of implementation. Beyond having a hand in the implementation, I have also manually tested this work.

@Chriztiaan Chriztiaan changed the title feat: Mutator pattern feat: Mutator example May 11, 2026
@Chriztiaan Chriztiaan mentioned this pull request May 13, 2026
Copy link
Copy Markdown

@rkistner rkistner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current mutator examples all assume a single row inserted/updated/deleted, and associates the _metadata with that operation directly.

A more generic approach could be to use a separate insertOnly table for the mutators, in which case you can do one (or more?) mutator per transaction instead. The SDK can then also handle the metadata automatically, instead of the mutator definitions needing to set _metadata.

Other notes:

  1. Should we support multiple mutators per transaction, or is that introducing too much complexity?
  2. Should we support batches or mutators in the API? Or even combine with /api/data in one unified call (once again depends on whether we allow multiple mutators in one transaction)?
  3. Supabase-specific: Can we use Supbase RPC calls as a simple way to define mutators? I.e. instead of having custom backend logic per mutator, just call a RPC function with a matching name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants