---
title: "Undo, Redo & Patches"
description: "Use the SDK's built-in undo stack, subscribe to patch events, and integrate with a host application's own history."
---

The SDK ships undo/redo by default for standalone sessions and emits RFC 6902 JSON patches on every change. Patch events are the integration seam for host application history, collaborative editing, audit logs, and embedded override mode.

## Built-in Undo and Redo

Standalone sessions (those without an `overrides` option) automatically attach a history module. You call `undo()` and `redo()` directly on the composition, and guard UI state with `canUndo()` and `canRedo()`.

```typescript
import { openComposition } from "@hyperframes/sdk";

const comp = await openComposition(html);

comp.setText("hf-title", "Draft A");
comp.setText("hf-title", "Draft B");

comp.canUndo(); // true
comp.undo();    // reverts "Draft B" → "Draft A"
comp.redo();    // reapplies "Draft B"
comp.canRedo(); // false — already at tip
```

<Tip>
  `canUndo()` and `canRedo()` return `false` when the stack is empty and also when `history: false` was passed to `openComposition`. Check them before showing undo/redo controls.
</Tip>

### Opting out of the built-in history

Pass `history: false` when the host application owns the undo stack and you want the SDK to emit patches without building a parallel internal stack:

```typescript
const comp = await openComposition(html, {
  overrides: storedOverrides,
  history: false,
});
// comp.undo() / comp.redo() are no-ops — host drives history via applyPatches()
```

<Note>
  `history: false` only disables the SDK undo stack. Persistence (auto-save) is independent — passing `history: false` does not suppress disk writes.
</Note>

### Coalescing rapid edits

The history module coalesces patch events into one undo entry when two conditions are met: the same op types and same element paths are touched within a configurable window. This prevents a slider drag from flooding the stack with hundreds of individual entries.

The default coalesce window is 300 ms. Override it in `openComposition`:

```typescript
const comp = await openComposition(html, {
  coalesceMs: 500, // ms — increase for slower interactions
});
```

For more on the coalesce contract and the `HistoryOptions` type, see [Utilities](/sdk/reference/utilities).

---

## Patch Events

Every committed change emits a `patch` event. Subscribe to mirror SDK edits into your host state, audit log, or collaboration channel.

```typescript
const off = comp.on("patch", ({ formatVersion, patches, inversePatches, origin, opTypes }) => {
  if (formatVersion !== 1) throw new Error("Unexpected patch format version");
  saveToDB({ patches, inversePatches, origin, opTypes });
});

// Unsubscribe when done
off();
```

### PatchEvent fields

<ResponseField name="formatVersion" type="1">
  Always `1` in the current SDK. Check this and reject unknown versions — a bump means a breaking change.
</ResponseField>
<ResponseField name="patches" type="JsonPatchOp[]">
  Forward patches that were applied: the add/remove/replace ops describing how the document changed.
</ResponseField>
<ResponseField name="inversePatches" type="JsonPatchOp[]">
  Inverse patches that undo this change. Store these alongside `patches` to support host-owned undo.
</ResponseField>
<ResponseField name="origin" type="unknown">
  The origin tag that was passed to `dispatch()` or `batch()`. Use it to route or filter events.
</ResponseField>
<ResponseField name="opTypes" type="string[]">
  Semantic names of the operations that produced this event (e.g. `["setStyle"]`). Useful for analytics and history labels. Not versioned — treat as informational.
</ResponseField>

### JsonPatchOp

The SDK emits and accepts only the add/remove/replace subset of RFC 6902:

```typescript
interface JsonPatchOp {
  op: "add" | "remove" | "replace";
  path: string;   // JSON Pointer (RFC 6901), e.g. "/elements/hf-title/style/color"
  value?: unknown;
}
```

The SDK never emits `move`, `copy`, or `test` ops, and `applyPatches()` ignores them if passed.

---

## Origin Model

Every mutation is tagged with an origin string. The SDK defines two built-in origins:

| Constant | Value | When used |
|----------|-------|-----------|
| `ORIGIN_LOCAL` | `"local"` | Default for all `dispatch()` and `batch()` calls |
| `ORIGIN_APPLY_PATCHES` | `"@hyperframes/sdk:applyPatches"` | Automatically applied by `applyPatches()` |

Both are exported from `@hyperframes/sdk`.

### Setting a custom origin

Pass `origin` in the options to `dispatch()` or `batch()` to tag the event for downstream routing:

```typescript
import { ORIGIN_LOCAL } from "@hyperframes/sdk";

comp.dispatch(
  { type: "setStyle", target: "hf-title", styles: { color: "#0EA5E9" } },
  { origin: "ui:color-picker" },
);

comp.batch(() => {
  comp.setText("hf-cta", "Buy Now");
  comp.setStyle("hf-cta", { backgroundColor: "#22C55E" });
}, { origin: "template-wizard" });
```

---

## Host-Owned History with applyPatches

When `history: false`, the host maintains its own undo stack from the patch events the SDK emits. To replay an undo step back into the SDK, call `applyPatches()` with the inverse patches from that stack entry:

```typescript
// Host undo stack entry (built from PatchEvent)
type HostEntry = {
  patches: JsonPatchOp[];
  inversePatches: JsonPatchOp[];
};

let history: HostEntry[] = [];

comp.on("patch", ({ patches, inversePatches, origin }) => {
  // Skip — this is the SDK replaying a host-initiated undo
  if (origin === ORIGIN_APPLY_PATCHES) return;
  history.push({ patches, inversePatches });
});

function hostUndo() {
  const entry = history.pop();
  if (!entry) return;
  comp.applyPatches(entry.inversePatches);
}
```

### Preventing undo loops

`applyPatches()` auto-tags its patch event with `ORIGIN_APPLY_PATCHES`. Your listener **must** skip that origin or you create an infinite loop:

```typescript
import { ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";

comp.on("patch", ({ patches, inversePatches, origin }) => {
  if (origin === ORIGIN_APPLY_PATCHES) return; // <-- required guard
  history.push({ patches, inversePatches });
});
```

<Warning>
  Omitting the `ORIGIN_APPLY_PATCHES` guard causes every `applyPatches()` call to push a new entry onto the host stack, which triggers another `applyPatches()`, and so on. Always check `origin` first.
</Warning>

`ORIGIN_APPLY_PATCHES` is a namespaced string (`"@hyperframes/sdk:applyPatches"`) rather than a `Symbol` so it survives `postMessage`, structured clone, and JSON round-trips — important when the host forwards patch events across frames or workers.

### applyPatches with a custom origin

If you want downstream listeners to receive an explicit marker different from the default:

```typescript
comp.applyPatches(entry.inversePatches, { origin: "host:undo" });
```

Any origin other than `ORIGIN_APPLY_PATCHES` will enter the history module if `history` is still attached. Use `history: false` or a `trackedOrigins` filter when running host-owned history to avoid double-stacking.

---

## Embedded Override Mode Integration

In embedded (T3) mode, the host stores only the sparse override delta for each user. Pair `history: false` with `applyPatches()` to let the host drive undo while the SDK updates the composition state:

```typescript
import { openComposition, ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";

const comp = await openComposition(baseTemplateHtml, {
  overrides: storedUserOverrides,
  history: false,
});

let hostStack: HostEntry[] = [];

comp.on("patch", ({ patches, inversePatches, origin }) => {
  if (origin === ORIGIN_APPLY_PATCHES) return;
  hostStack.push({ patches, inversePatches });
  // Persist the updated override set
  saveOverrides(comp.getOverrides());
});

function undo() {
  const entry = hostStack.pop();
  if (entry) comp.applyPatches(entry.inversePatches);
}
```

See [Embedded Override Mode](/sdk/guides/embedded-override-mode) for the full T3 integration pattern.

---

<CardGroup cols={2}>
  <Card title="Composition API" icon="code" href="/sdk/reference/composition">
    Full method signatures for `undo`, `redo`, `canUndo`, `canRedo`, `on`, `applyPatches`, `dispatch`, and `batch`.
  </Card>
  <Card title="Types" icon="brackets-curly" href="/sdk/reference/types">
    `PatchEvent`, `JsonPatchOp`, `ORIGIN_APPLY_PATCHES`, `ORIGIN_LOCAL`, and related types.
  </Card>
  <Card title="Utilities" icon="wrench" href="/sdk/reference/utilities">
    `HistoryOptions`, `coalesceMs`, `trackedOrigins`, and `createHistory` internals.
  </Card>
  <Card title="Embedded Override Mode" icon="layer-group" href="/sdk/guides/embedded-override-mode">
    Full T3 embedded integration with host-owned history.
  </Card>
</CardGroup>

