Every record has a state field on its BaseModel. The repository service treats state as first-class: it gates visibility, drives the approval flow when enabled, and fires events that automations subscribe to. Understanding the state machine is half of understanding AppEngine.
The built-in states
ModelState from @jaclight/dbsdk:
enum ModelState {
draft, new, pending, inprogress, reviewed, approved,
published, completed, hold, rejected, cancelled, archived, deleted
}
Most code only touches three:
draft— the record exists but isn't visible to public/storefront endpoints. Default for anything authored.published— visible everywhere. The product appears in the catalog, the page renders on the site, the campaign starts sending.archived— soft-deleted, hidden from default queries. Recoverable viaPOST /repository/trash-restore.
The other states only matter when a collection has approval workflow turned on: pending → inprogress → reviewed → approved → published, with rejected and hold as branches.
The publish lifecycle
The repository exposes three lifecycle endpoints:
/repository/publish/{datatype}/{id}JWT/repository/unpublish/{datatype}/{id}JWT/repository/approve/{datatype}/{id}JWTpublish flips state to published and stamps publishedDate. unpublish returns it to draft. approve is the workflow analog — it moves the record forward in the approval pipeline.
You can also write state directly via the partial-update endpoint, but going through the lifecycle endpoints is preferred — they emit the right events.
curl -X POST https://appengine.appmint.io/repository/publish/page/page-abc123 \
-H "orgid: my-org" \
-H "Authorization: Bearer $JWT"
How state is enforced on reads
Public endpoints filter by state by default. GET /storefront/products won't return draft products. GET /site/page/{slug} skips unpublished pages. Authenticated admin endpoints (under /repository/*) return all states; the caller is expected to filter as needed.
When you query through /repository/find/{datatype}, you can opt into a state filter:
{
"filter": { "data.category": "shoes" },
"modelState": ["published"]
}
modelState accepts a single state or an array. Without it, you get everything.
Approval workflow (opt-in)
Collections with enableWorkflow: true route writes through an approval pipeline instead of letting them publish directly. The endpoints:
/repository/request-approval/{datatype}/{id}JWT/repository/approve/{datatype}/{id}JWT/repository/reject/{id}JWTCalling request-approval sets workflow.status = pending and dispatches a WorkflowUpdateCommand. A reviewer with the right requiredRole.approve permission then calls approve (or reject). On approval, the record advances; on rejection it returns to draft with a reason recorded in the workflow trail.
This machinery is implemented in WorkflowService and dispatched via NestJS's CommandBus. It's the same engine used by Storefront returns/RMA, CRM ticket escalation, and CMS page approval.
Workflow is a per-collection toggle. Most built-in collections leave it off; turn it on for content that needs editorial review (pages, campaigns, sensitive customer-facing copy).
Custom states
The state field is typed as ModelState but the repository doesn't validate the string against the enum — you can write any value. Custom states show up in queries via the same modelState filter.
Use cases:
- A
leadcollection might wantcold | warm | hot | convertedinstead of the default lifecycle. - A
crm-ticketflow needsopen | acknowledged | resolved | closed. - A
subscriptionflow needstrialing | active | past_due | cancelled.
Most production setups define their workflow states in the collection's data.workflow config, render them in admin UIs as a kanban or stepper, and use them as the trigger for automations.
Events that fire on state transitions
Every state change emits a domain event on the NestJS event bus. The four primary events on writes:
collection-create— fires on insert, regardless of starting state.collection-update— fires on every update.collection-publish— fires when state moves topublished.collection-approve— fires when the approval pipeline marksapproved.collection-delete— fires on hard delete.
Defined in src/enums/system-events-enum.ts. The Automation module subscribes to these to trigger flows ("when an order is published, send the confirmation email"). The Sync module subscribes to push notifications, social posts, or external integrations.
You can wire your own listener anywhere with NestJS's @OnEvent:
@OnEvent('collection-publish')
handleProductPublish(payload: { orgid: string; data: BaseModel<any> }) {
if (payload.data.datatype === 'product') {
// index in search, push to sales channels, ...
}
}
See Events and async for the full catalog.
History and restore
Collections with enableHistory: true keep a version of every change. Read history with GET /repository/history/{datatype}/{id} and restore a previous version with POST /repository/history/restore.
/repository/history/{datatype}/{id}JWT/repository/history/restoreJWTFor soft-deleted records, POST /repository/trash-restore brings them back from the archived (or deleted) state.
/repository/trash-restoreJWT