---
number: 12174
title: >-
  feat(data): backend-first remoteCreate/remoteUpdate/remoteDestroy on Store
  (#12173)
author: neo-opus-ada
state: MERGED
createdAt: '2026-05-29T16:10:44Z'
updatedAt: '2026-05-29T16:31:40Z'
closedAt: '2026-05-29T16:31:35Z'
mergedAt: '2026-05-29T16:31:35Z'
head: store-remote-crud
base: dev
url: 'https://github.com/neomjs/neo/pull/12174'
---
Authored by Claude Opus 4.8 (Claude Code). Memory-core orchestrator deactivated this session — no Origin Session ID.

FAIR-band: operator-directed PR (explicit request to implement #12173) — not autonomous lane self-selection.

Resolves #12173

Adds the async backend-first CRUD layer on `Neo.data.Store`: `remoteCreate` / `remoteUpdate` / `remoteDestroy`. The sync mutators (`add`/`insert`/`remove`) stay sync; the new methods call `api.{create,update,destroy}` **first** and only mutate locally on success (applying the server-authoritative response), reject + fire a `mutationFailed` event on failure, and fall back to the local op when the matching `api` verb is not configured. No RPC plumbing was required — the api dispatch is already method-generic (`register()` auto-stubs every descriptor method; `Message.onMessage` routes by method+type), so `create`/`update`/`destroy` dispatch like `read`.

Evidence: L1 (unit: success / backend-failure / no-api fallback across the three verbs, with a fake api service registered in-namespace) → L1 required (no runtime-only ACs on the neo side; the live websocket round-trip is exercised by the consuming app's CRUD wiring, tracked separately). No residuals.

## Deltas from ticket

- `remoteUpdate(record, data)` takes the changed `data` explicitly (the caller passes the form values) rather than auto-deriving the `trackModifiedFields` delta. There is no public Record delta accessor yet, and the explicit form is cleaner for the Save/Cancel dialog flow. Auto-delta is deferred as a follow-up enhancement.
- Added `remoteRpc(verb, payload)` as a `@protected` helper (resolves the stub and awaits `Neo.remotes.Api.ready()` when racing registration, mirroring `load()`).
- The `api` config JSDoc already documented `{create, read, update, destroy}`, so no config change was needed — only the consuming methods were missing.

## Test Evidence

- `npm run test-unit -- test/playwright/unit/data/StoreRemoteCrud.spec.mjs` → **7 passed**
- `npm run test-unit -- test/playwright/unit/data/` → **63 passed** (no regression across Store/TreeStore)

## Post-Merge Validation

- [ ] A consuming app wires its CRUD dialogs to `remoteCreate`/`remoteUpdate`/`remoteDestroy` over a live remotes-api websocket and persists create/update/delete end-to-end (tracked in that app's own backlog).
