# Options (/docs/schema/collections/options)

---
title: Options
description: One .options() call decides whether a collection stamps timestamps, keeps deleted rows, keeps history, moves through publish stages, and which Postgres schema its tables live in.
kind: guide
package: questpie
---

Which of these switches do you actually want? They sound alike and guarantee
different things, and picking the wrong one is silent. Nothing errors, you just
do not get what you assumed.

## One call, every option

`.options()` replaces the whole options object, so a second call throws away
the first. Pass everything at once.

```ts title="src/questpie/server/collections/pages.ts"
import { collection } from "#questpie/factories";

export const pages = collection("pages")
	.fields(({ f }) => ({ title: f.text().required() }))
	.title(({ f }) => f.title)
	.options({
		timestamps: true, // createdAt + updatedAt, on unless you say false
		softDelete: true, // deletedAt column, deletes stop removing rows
		versioning: { maxVersions: 100, workflow: true },
		schema: "content", // Postgres schema, default public
	});
```

`CollectionOptions` is
`{ schema?, timestamps?, softDelete?, optimisticConcurrency?, versioning?, realtime? }`.

## Which one do you need

| You want                                     | Switch                  |
| -------------------------------------------- | ----------------------- |
| When a row was created or last changed       | `timestamps`            |
| A deleted row you can bring back             | `softDelete`            |
| Past states you can read and revert to       | `versioning`            |
| Legal moves between draft and published      | `versioning.workflow`   |
| A write rejected because it read a stale row | `optimisticConcurrency` |
| Tables in a schema other than `public`       | `schema`                |

Versioning is not a lock. It records what happened after the write lands. Two
concurrent updates both succeed, both write a version row, and the second wins.
`optimisticConcurrency` is the separate switch that rejects the loser.

## timestamps

`boolean`, default **`true`**. Adds `createdAt` and `updatedAt`, both
`notNull` with `defaultNow`. Set `timestamps: false` explicitly to drop them.

## softDelete

`boolean`, default `false`. Adds a nullable `deletedAt` column and two indexes,
one on `deletedAt` and a retention index on `(deletedAt, id)` limited to
`WHERE deleted_at IS NOT NULL`. With it on:

- `deleteById` and `deleteMany` stamp `deletedAt` instead of removing the row.
- `restoreById` brings a soft-deleted row back.
- `purgeById` removes one for good, under its own `purge` access rule. Delete
  permission never grants purge, and an active row is rejected.
- Reads hide deleted rows. Pass `includeDeleted: true` to see them.

For a unique value another row should be able to reclaim after a delete, use
`softDeleteUniqueIndex` from `questpie` inside `.indexes()`. Called as
`softDeleteUniqueIndex("users_email_unique", table.deletedAt, table.email)`, it
scopes the constraint to `WHERE deleted_at IS NULL`.
[Soft delete](/docs/schema/soft-delete) covers retention and purge in full.

## versioning

`boolean | CollectionVersioningOptions`, default `false`. The object form is
`{ enabled?, maxVersions?, workflow?, collaborativeSnapshots? }` and
`maxVersions` defaults to **50**. Turning it on creates a `<name>_versions`
table, plus `<name>_i18n_versions` when the collection has localized fields,
and makes `findVersions` and `revertToVersion` do something.

## Publish stages

Nest `workflow` under `versioning`. `workflow: true` gives you the stages
`["draft", "published"]`. The object form names the stages and limits the moves
between them.

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

`WorkflowOptions` is `{ stages?, initialStage? }`, where `stages` takes either a
string array or a keyed object of `{ label?, description?, transitions? }`.
Omit `transitions` and a stage may move to any other. `initialStage` defaults to
the first stage, and naming one that is not in `stages` throws when the app
starts, as does a `transitions` entry pointing at an unknown stage.

Workflow does not add the surface, it switches it on. `transitionStage()`, the
`beforeTransition` and `afterTransition` hooks, the `access.transition` rule
and the `versionStage` and `versionFromStage` columns are all there without
it. Leave workflow off and `transitionStage()` throws, the hooks never fire
and both stage columns stay null.

<Callout type="warn" title="Workflow lives under versioning">
	Stage moves write version snapshots, so setting `workflow` turns versioning
	on. Setting `versioning: { enabled: false }` together with a `workflow`
	throws when the collection is built.
</Callout>

A stage carries no condition of its own. `WorkflowStageOptions` is shape only.
Put a conditional rule in a `beforeTransition` hook, which aborts when it
throws, or in `access.transition` when the question is who may move the record
rather than what its data says.

## optimisticConcurrency

`true` only. It generates a framework-owned `revision` column, and every
mutation of an existing row then requires `expectedRevision`. Bulk mutations
require exact per-id `expectedRevisions`. Localized-only, relation-only,
workflow, restore and revert writes all advance the same canonical revision
once. Declaring your own field called `revision` throws.

`.collaborative()` turns this on for you, since a collaborative row cannot be
written blind. See
[Optimistic concurrency](/docs/schema/collections/optimistic-concurrency).

## schema

`string`, default `public`. All four tables for the collection (main, i18n,
versions, i18n versions) are created in that Postgres schema, and generated
migrations emit `CREATE SCHEMA IF NOT EXISTS` first. Relations across schemas
render fully qualified, as `REFERENCES "other_schema"."table"("id")`.

## Next

**[Reading and writing](/docs/schema/collections/crud)** shows the methods
these options unlock, and the REST route behind each one.
