docs
/
Platform

Data model

BaseModel, the fields you get for free on every record, and how collections and subschemas work.

One envelope for everything

There is no per-feature schema at the storage layer. Every record — a product, an employee, a community post, a bank transfer — is a BaseModel<T> where T is the feature-specific payload.

interface BaseModel<T> {
  pk: string;
  sk: string;
  name: string;
  data?: T;

  datatype?: DataType | string;
  subschema?: string;
  version: number;

  state?: ModelState;
  createdate?: Date;
  modifydate?: Date;
  publishedDate?: Date;

  author?: string;
  created_by?: string;
  modified_by?: string;
  owner?: { datatype?: DataType; id?: string; name?: string; email?: string };

  requiredRole?: RequiredRoleModel;
  workflow?: TaskModel;
  rules?: any[];
  notes?: { author: string; comment: string; date: Date }[];

  stats?: BaseModelStats;
  share?: BaseModelShare;
  post?: PostSubModel;
  style?: StyleSubModel;

  search?: string;
  create_hash?: string;
  client?: string;
  isNew?: boolean;
}

Keys

FieldTypeDescription
sk requiredstringThe record id. This is what /repository/get/:datatype/:id takes, what author and owner.id point at, and what the guard compares for ownership. Not _id.
pk requiredstringPartition key. Groups related records.
name requiredstringHuman-readable label. Present on every record regardless of datatype.
datatype`DataTypestring`
subschemastringA variant within the datatype. Addressed in URLs as datatype-subschema.
The system-owned field list is exported as baseModelSystemFields — useful when you need to separate platform fields from a user's own.

What every record gets for free

Because these live on BaseModel rather than on individual features, they work identically for all 261 datatypes.

Lifecycle

state moves through ModelState:

draft → new → pending → inprogress → reviewed → approved → published → completed

with hold, rejected, cancelled, archived and deleted as off-ramps. Driven by:

GET/repository/request-approval/:datatype/:idJWT
POST/repository/approve/:datatype/:idJWT
POST/repository/reject/:idJWT
POST/repository/publish/:datatype/:idJWT
POST/repository/unpublish/:datatype/:idJWT

Versioning and history

version increments on write; prior revisions are retrievable and restorable.

GET/repository/history/:datatype/:idJWT
POST/repository/history/:restoreJWT

Deletes are recoverable — deleted records land in the trash datatype.

POST/repository/trash-restoreJWT

Row-level permissions

requiredRole names the content permissions needed per operation:

{ read?: string[]; create?: string[]; update?: string[]; delete?: string[]; review?: string[]; approve?: string[] }

JwtAuthGuard reads requiredRole.update and requiredRole.delete directly. Set them and a record enforces its own access rules, independent of the handler's decorators.

Engagement

stats is on every record, so any datatype can be liked, viewed or rated without new storage:

{ likes?, dislikes?, views?, shares?, bookmarks?, follows?,
  averageRating?, ratingCount?,
  reactions?: { author, reaction, reacted_at }[],
  reactionSummary?: { [type: string]: number },
  last_viewed?, last_activity? }

The stats controller (/stats/*, part of AnalyticsModule) is the generic engine that maintains it.

Sharing

share is a tokenized public link with optional passcode, expiry and notification:

{ token: string; status: 'active' | 'revoked'; passcodeHash?: string; expiresAt?: string;
  notify?: { channel: 'sms' | 'email' | 'whatsapp'; to: string; sentAt?: string }[];
  createdBy?: string; createdAt?: string; openedAt?: string }

openedAt records first open — enough to tell whether a shared quote was ever looked at.

Publishing metadata

post carries the content-publishing block used by pages, posts and any shareable record: title, summary, allowShare, allowComment, allowRating, showRelated, categories (tree-selected from the category collection), tags (from tag), and images.

images is a file reference with pre-generated size variants:

{ path, url, contentType, isPublic,
  meta: { width, height, size, xs: { path, url }, sm: { path, url }, md: { path, url } } }

Uploads produce those variants automatically — see Files.

Styling

style lets a record carry its own presentation: a theme (name, darkMode of auto/dark/light, property/value settings), plus raw classes, css, javascript, and arrays of styleLinks / scriptLinks validated against ^https?://.

Schema definition

Schemas are JSON Schema with x- extensions that drive UI generation — the same document validates the data and renders the form.

ExtensionPurpose
x-controlWhich control to render (ControlType.selectMany, .file, .code…)
x-control-variantVariant — textarea, chip, combo, tree, css, javascript
x-renderHow to render the value when displaying
dataSourceWhere options come from: { source: 'collection', collection, value, label, children } or { source: 'function', value }
hidden, hideIn, readOnlyVisibility, optionally scoped to a context such as generator
collapsible, group, layout, styleClass, stylingLayout hints

This is why the Studio form builder needs no separate UI metadata — it renders these documents directly.

Collections

A collection is a user-defined datatype, stored as a collection record holding its schema.

GET/repository/collections/:name?/:subName?JWT
POST/repository/collections/fixJWT

collections/fix reconciles a collection with its schema — creating the MongoDB collection and its indexes if they are missing. createCollection also has a time-series variant for metric-shaped data.

Once defined, a custom collection is addressed exactly like a built-in datatype.

Query responses

List endpoints return BaseModelDTO<T>:

interface BaseModelDTO<T> {
  data?: BaseModel<T>[];
  total?: number;
  datatype?: DataType | string;
  page?: number; pageSize?: number; lastPage?: number; lastItem?: number;
  hasNext?: boolean;
  sort?: any; sortType?: SortType;      // asc = 1, desc = -1
  modelState?: ModelState | ModelState[];
  fromCache?: boolean;
  error?: { message: string; code: number; stalk: any };
}

DataOptions — the request-side half — additionally accepts refresh, enrich, random, and field controls includeFields, excludeFields, maskFields.

enrich resolves references

With enrich, the repository follows references and inlines related records (repository.enrichment.service.ts) rather than returning bare ids.