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.
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.
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:
deleteByIdanddeleteManystampdeletedAtinstead of removing the row.restoreByIdbrings a soft-deleted row back.purgeByIdremoves one for good, under its ownpurgeaccess rule. Delete permission never grants purge, and an active row is rejected.- Reads hide deleted rows. Pass
includeDeleted: trueto 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.
Reading and writing
Every collection produces the same typed CRUD object and the same set of REST routes, so one vocabulary works across your whole schema.
Uploads
Calling .upload() turns a collection into the place file bytes live, adding the storage columns, a resolved url on every row, the upload methods and the file routes.