QUESTPIE
SchemaCollections

Options

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.

View markdown

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.

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 wantSwitch
When a row was created or last changedtimestamps
A deleted row you can bring backsoftDelete
Past states you can read and revert toversioning
Legal moves between draft and publishedversioning.workflow
A write rejected because it read a stale rowoptimisticConcurrency
Tables in a schema other than publicschema

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 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.

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.

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.

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.

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 shows the methods these options unlock, and the REST route behind each one.

On this page