# Versions and stages (/docs/schema/globals/versions)

---
title: Versions and stages
description: Turning on versioning snapshots every write of a global into a history table, and nesting a workflow under it adds draft and published stages you move between.
kind: guide
package: questpie
---

Someone edits the footer at four on a Friday and the wrong link goes live. You
want the previous copy back, and next time you want the change to sit in draft
until an editor releases it. Both come from one option.

## Keeping a history

```ts title="src/questpie/server/globals/site-settings.ts"
import { global } from "#questpie/factories";

export const siteSettings = global("site_settings")
	.fields(({ f }) => ({ siteName: f.text().required().default("My Site") }))
	.options({ versioning: true });
```

Every write now snapshots the row into `site_settings_versions`, numbered from
one. Two methods work on that history:

```ts
const history = await app.globals.site_settings.findVersions({ limit: 10 });
//    oldest first, each entry carries the field values of that snapshot
//    plus versionId, versionNumber, versionOperation, versionUserId,
//    versionCreatedAt and sourceRevision

await app.globals.site_settings.revertToVersion({ version: 3 });
```

`revertToVersion()` also takes `{ versionId }`. It needs one of the two and
throws `400` without either. `findVersions()` enforces the same `read` rule as
`get()`, and returns an empty array when versioning is off.

The server type for `findVersions()` declares the metadata only, so TypeScript
will not show you the field values the query does return. The client's type
carries them.

<Callout type="info" title="History is capped">
	`maxVersions` defaults to 50. Older snapshots are deleted as new ones land.
	Raise it with `versioning: { maxVersions: 200 }`.
</Callout>

## Draft and published

Nest a `workflow` under versioning. Stages are stored as version snapshots, so
they cannot exist without it.

```ts
.options({ versioning: { workflow: true } })   // stages: draft, published
```

Writes land on the initial stage, `draft`. Move the global forward on its own,
without touching the data:

```ts
await app.globals.site_settings.transitionStage({ stage: "published" });

const live = await app.globals.site_settings.get({ stage: "published" });
```

Reading a stage other than the initial one reads the newest snapshot carrying
that stage, so `get({ stage: "published" })` returns `null` until a snapshot
lands there. Reading without a `stage` returns the live row.

### Custom stages

```ts
.options({
	versioning: {
		workflow: {
			stages: ["draft", "review", "published"],
			initialStage: "draft",
		},
	},
})
```

`stages` also takes a keyed object, `{ draft: { transitions: ["review"] }, … }`,
where `transitions` lists the stages you may move to next. Leave `transitions`
off and any stage can follow any other. An `initialStage` or a transition target
outside `stages` throws the first time the global is used, not at boot. Asking
for an unknown stage at runtime, or making a move `transitions` forbids, throws
`400`.

### What a workflow adds

| Thing                                | Effect                                                                                   |
| ------------------------------------ | ---------------------------------------------------------------------------------------- |
| `transitionStage()`                  | Moves stage and writes a snapshot, no data change                                        |
| `beforeTransition` `afterTransition` | Both see `fromStage` and `toStage`. Only the first aborts on throw, the second is logged |
| `access.transition`                  | Its own rule, falling back to `access.update`                                            |
| `stage` on `get()` and `update()`    | Which snapshot to read, which stage to write to                                          |

Pass `scheduledAt` with a future date and the transition is queued instead of
run, after the target stage, the record, the access rule and the transition
graph have all been checked. This needs a queue adapter and throws without one.

## Optimistic concurrency

`optimisticConcurrency: true` adds a framework-owned `revision` column and makes
every write name the revision it expects.

```ts
.options({ versioning: true, optimisticConcurrency: true })
```

```ts
await app.globals.site_settings.update({
	data: { siteName: "QUESTPIE" },
	expectedRevision: 1,
});
```

The patch moves inside a `data` key, and `expectedRevision` sits beside it.
`revertToVersion()` and `transitionStage()` take it too. A mismatch throws
`409`. An absent row counts as revision `0`, and the row starts at `1` once it
exists. You cannot declare a field called `revision`, the constructor throws.

Full behaviour is on
[Optimistic concurrency](/docs/schema/collections/optimistic-concurrency).

<Callout type="warn" title="Switching versioning off breaks a workflow">
	`versioning: { enabled: false, workflow: true }` fails when the app builds the
	global. Stages are version snapshots, so switching versioning off removes the
	thing they are stored in.
</Callout>

## The tables

`<name>_versions` holds `versionId` as its primary key, the source row's `id`,
`versionNumber`, `sourceRevision`, `versionOperation`, `versionStage`,
`versionFromStage`, `versionUserId`, `versionCreatedAt`, and a copy of every
unlocalized field plus the source row's timestamps. It is indexed on
`(id, version_number)`,
`(id, version_stage, version_number)` and `version_created_at`.

Localized fields go to `<name>_i18n_versions`, one row per locale per version,
unique on `(parent_id, version_number, locale)`.

## Related

- **[Globals](/docs/schema/globals)** is the page this one hangs off.
- **[One row per tenant](/docs/schema/globals/scoped)** carries `scope_id` into
  the versions table too.
