Every record AppEngine reads or writes is wrapped in BaseModel\<T\>. The wrapper carries identity, versioning, lifecycle state, and audit metadata; the payload you actually care about sits at data. Master this shape once and you've understood the data layer.
The shape
From @jaclight/dbsdk (models/base.model.d.ts):
interface BaseModel\<T\> {
pk: string; // primary key (record id)
sk: string; // sort key (org-scoped namespace)
name: string; // human label
data?: T; // your domain payload
datatype?: string; // collection name
subschema?: string; // sub-type within a collection
version: number; // optimistic concurrency token
state?: ModelState; // workflow state (draft, published, ...)
isNew?: boolean;
workflow?: TaskModel;
post?: PostSubModel; // share/comment/category settings
style?: StyleSubModel; // CMS-style styling (theme, css)
createdate?: Date;
modifydate?: Date;
publishedDate?: Date;
author?: string;
owner?: { datatype?; id?; name?; email? };
requiredRole?: RequiredRoleModel; // per-record RBAC overrides
comments?: CommentModel[];
activities?: ActivityModel[];
messages?: MessageModel[];
shares?: number;
likes?: number;
dislikes?: number;
averageRating?: number;
ratingCount?: number;
reactions?: any[];
search?: string; // denormalized search blob
views?: number;
last_viewed?: Date;
modified_by?: string;
created_by?: string;
client?: string;
}
The system reserves these fields under baseModelSystemFields — never put your own properties at the top level; they go inside data.
pk vs sk
pk is the record's identity. It's stable, unique within an org, and what you pass to /repository/get/{datatype}/{id}.
sk is the org-scoped sort key — typically <orgid>/<datatype> or a hierarchical path. The repository layer uses it to:
- enforce tenant isolation (an
skalways begins with the orgid), - group records for efficient range queries,
- support hierarchical reads (e.g. all pages under a site).
You almost never construct sk yourself. RepositoryService.createNewData() and the repository providers fill it in. Read it; don't write it.
Optimistic concurrency
Every write increments version. To update safely, send the version you read:
// fetch
const record = await fetch('/repository/get/contact/abc123', { headers });
const body = await record.json();
// modify
body.data.email = '[email protected]';
body.version; // e.g. 4 — must be sent back
// write
await fetch('/repository/update/abc123', {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
If another writer has bumped the record since your read, the update will reject with a version conflict. Refetch, reapply, retry.
ModelState — workflow
state drives the publishing/workflow machinery. The enum lives in dbsdk:
enum ModelState {
draft, new, pending, inprogress, reviewed, approved,
published, completed, hold, rejected, cancelled, archived, deleted
}
Most domain code only cares about draft, published, and archived. The rest are used by the approval flow when a collection has enableWorkflow: true. See State and workflow.
Reading a record
curl https://appengine.appmint.io/repository/get/contact/contact-abc123 \
-H "orgid: my-org" \
-H "Authorization: Bearer $JWT"
/repository/get/{datatype}/{id}JWT+APIKEYWriting a record
POST /repository/create for new records, POST /repository/update/{id} for existing ones.
curl -X POST https://appengine.appmint.io/repository/create \
-H "orgid: my-org" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{
"datatype": "contact",
"data": { "email": "[email protected]", "name": "Alice" }
}'
/repository/createJWT+APIKEY/repository/update/{id}JWT+APIKEYThe server fills in pk, sk, version, createdate, modifydate, and state. You provide datatype and data.
Sending { "email": "...", "name": "..." } at the top level won't work — those go inside data. The system fields are reserved.
Partial updates
For single-field updates, use the partial endpoint — no version check, but smaller payload.
/repository/update-partial/{datatype}/{id}JWT+APIKEY{ "data.email": "[email protected]" }
The server merges this into the existing record. Use it for status flips and counter increments; use /repository/update/{id} when you care about concurrency.