Data Model
Everything in DMART is an Entry — a single, uniformly addressed unit of information. There are no bespoke tables per feature, no separate "users API" versus "documents API": a user, a folder, a blog post, a support ticket, an uploaded PDF and a system log are all the same shape of object, distinguished only by a resource_type. Understand the Entry and its address, and you understand the whole platform.
1. The Locator — how everything is addressed
Every entry is uniquely identified by a four-tuple called the Locator. Think of it as the full postal address of a piece of data. If you know these four values, you can read, update, or delete the entry — no numeric primary key required.
Locator = ( resource_type , space_name , subpath , shortname )
│ │ │ │
│ │ │ └ "article_42" name, unique in the folder
│ │ └─────────── "/blog/2026" folder path in the space
│ └─────────────────────── "myblog" the space (tenant)
└───────────────────────────────────── "content" what kind of entry it is
| Part | Analogy | Rules |
|---|---|---|
resource_type |
File type / class | One of the 30 resource types (see §6). Discriminates the entry's shape and which table/validation applies. |
space_name |
Drive / tenant | Top-level container. Every entry belongs to exactly one space. |
subpath |
Folder path | Hierarchical, slash-separated (e.g. /users/employees). Stored with a leading slash internally; the leading slash is stripped on the wire. |
shortname |
Filename | Unique within (space, subpath). Allowed characters: a–z A–Z 0–9 _ plus Arabic letters and Arabic-Indic digits, 1–64 chars. |
The Locator is a first-class object, not just a convention. Two entries can share a shortname as long as any other part of the tuple differs.
2. Spaces & Subpaths
graph TD
Root["DMART instance"] --> S1["Space: management"]
Root --> S2["Space: applications"]
Root --> S3["Space: myblog (yours)"]
S3 --> P1["subpath: /"]
S3 --> P2["subpath: /blog"]
P2 --> P3["subpath: /blog/2026"]
P3 --> E1["entry: article_42 (content)"]
P3 --> E2["entry: article_43 (content)"]
P2 --> F1["entry: 2026 (folder)"]
Space — the tenant boundary
A Space is the top-level organizational unit and the unit of isolation. It is itself an entry (resource_type=space) and carries configuration: which languages it supports, which plugins are active, whether indexing is enabled, whether it is hidden, its display ordinal, and which subfolders to hide.
management Always present. Holds users, groups, roles, permissions and the meta-schemas. Bootstrapped on first run.
applications Built-in catalog of app definitions, saved queries, configurations and translations.
personal Per-user private space for user-owned content.
your spaces Any number of custom spaces you create for your own domains.
Subpath — the folder hierarchy
A Subpath is a slash-separated path inside a space, exactly like a directory path in a filesystem. / is the root of the space. Subpaths are normalized (leading/trailing slashes handled consistently) so blog/2026, /blog/2026 and /blog/2026/ all resolve to the same folder.
A subpath only "exists" as a browsable, configurable thing when a folder entry represents it. That folder is what carries display and behavior configuration for its contents — covered in depth on the Folders & Rendering page.
3. The Entry — four layers
Every entry is composed of up to four layers. Only Meta is mandatory; the rest are optional and added as needed.
graph LR
Entry["Entry"] --> Meta["① Meta<br/>identity, ownership,<br/>timestamps, tags"]
Entry --> Payload["② Payload<br/>JSON body (schema-validated)<br/>or file reference"]
Entry --> Att["③ Attachments<br/>comments, media, reactions,<br/>relationships, locks, shares"]
Entry --> Hist["④ History<br/>immutable diff<br/>of every change"]
① Meta System-level identity: uuid, shortname, ownership, ACLs, tags, display names, timestamps. Always present.
② Payload The actual content — a JSON object validated against a schema, or a pointer to an attached file. Optional.
③ Attachments Secondary entries bound to this one: comments, media, reactions, relationships, locks. Optional, unbounded.
④ History Append-only diff log. Every create/update/delete writes a row; the entry points at the latest one.
4. Meta — the fields every entry shares
Regardless of resource type, every entry inherits this common set of fields (the Meta base). Type-specific fields are layered on top (see §5).
| Field | Type | Purpose |
|---|---|---|
shortname |
string | Identifier, unique within (space, subpath). |
uuid |
UUID | Permanent primary key. Never changes, even on rename or move. |
is_active |
bool | Soft-active flag. The is_active permission condition checks it. |
slug |
string | Optional URL-friendly alias. |
displayname |
Translation | Per-language labels, e.g. { "en": "Articles", "ar": "مقالات" }. |
description |
Translation | Per-language descriptions. |
tags |
string[] | Free-form labels. Queryable with @tags:foo. |
owner_shortname |
string | The user who owns the entry. |
owner_group_shortname |
string | Owning group. Used by the own permission condition. |
created_at |
timestamp | Set on insert. |
updated_at |
timestamp | Refreshed on every write. |
payload |
Payload | Content type, schema reference, and body (see §5). |
acl |
Acl[] | Per-entry access-control overrides: {user_shortname, allowed_actions}. |
relationships |
Relationship[] | Typed links to other entries. |
last_checksum_history |
string | Pointer to the latest row in the history log. |
query_policies |
string[] | Precomputed ACL policy strings for fast read-time filtering. |
5. Payload & type-specific fields
The Payload is where an entry's real content lives. It is one of two things:
Inline JSON — a structured object. If
schema_shortnameis set, the body is validated against that JSON Schema on every write.A file reference —
bodyholds a filename and the bytes are fetched separately (images, PDFs, CSV, Parquet, …).
"payload": {
"content_type": "json", // json | markdown | html | image | pdf | csv | parquet | ...
"schema_shortname": "article", // optional — enables JSON-Schema validation
"body": { // inline object, OR a filename string for file payloads
"title": "Hello World",
"body": "First post."
}
}
Type-specific extensions
Selected resource types add their own fields on top of Meta:
| Type | Adds |
|---|---|
user |
password (hashed, never serialized), roles, groups, email, msisdn, OAuth ids, locked_to_device, verification & login state. |
role |
permissions — list of permission shortnames. |
permission |
subpaths, resource_types, actions, conditions, restricted_fields, allowed_fields_values. |
ticket |
state, is_open, workflow_shortname, reporter, collaborators, resolution_reason. |
space |
languages, active_plugins, indexing_enabled, hide_folders, hide_space, ordinal, icon, mirrors. |
folder |
A payload validated against the folder_rendering schema — how its contents are displayed. See Folders & Rendering. |
6. Resource-type catalog
There are 30 resource types. They all share the Meta base; the type only changes which extra fields exist, which table stores the entry, and which validation runs.
Identity user, group
Structure folder, space
Content content, schema, data_asset, csv, jsonl, sqlite, parquet
Workflow ticket
Social comment, reply, post, reaction, notification, share
Attachments media, log, relationship, alteration, history, lock
Management role, permission, acl
Extensions locator, json, plugin_wrapper
On the wire, resource types are lowercase snake_case strings (plugin_wrapper, data_asset).
7. Attachments — sub-entries bound to a parent
Attachments are themselves entries, stored in a dedicated table, that point back at a parent entry. They keep related information physically together without polluting the parent's payload.
| Attachment | What it is |
|---|---|
media |
An uploaded file (image, PDF, video, document) bound to the entry. |
comment / reply |
Threaded discussion on the entry. |
reaction |
An emoji / like reaction. |
relationship |
A typed link to another entry (see also the Meta relationships field). |
lock |
An advisory/enforced lock preventing concurrent edits (see Entity Lifecycle). |
share |
A share grant exposing the entry to another party. |
alteration |
A proposed/tracked change record. |
Every attachment carries an author_locator (who created it), optional media bytes, a text body, and a state.
8. History & alterations
DMART never silently overwrites. Every create, update, and delete writes a diff to the history log alongside the request headers that caused it. The entry's last_checksum_history field chains to the most recent entry, forming an auditable, tamper-evident trail.
Query an entry's history via the
historyquery type.The diff captures exactly which fields changed, from what to what.
Because history is append-only, you get a complete audit log for free.
9. On-disk layout (the .dm convention)
DMART's live data lives in PostgreSQL, but it can be exported to — and seeded from — a human-readable folder tree, and the sample/fixture data ships as that tree. This .dm layout is the export, seed and backup representation of the model, not a runtime store. Understanding it demystifies the whole model, because the directory structure is the data model made visible.
spaces/
└── myblog/ ← a Space
├── .dm/
│ └── meta.space.json ← space meta (languages, plugins, ordinal…)
│
├── blog.json ← the FOLDER BODY for /blog (rendering config)
├── blog/ ← the /blog subpath
│ ├── .dm/
│ │ └── meta.folder.json ← folder meta (points at ../blog.json as its body)
│ │
│ ├── article_42.json ← a content entry's payload body
│ └── .dm/
│ └── article_42/
│ ├── meta.content.json ← the entry's meta
│ └── attachments.comment/ ← attachments live beside the entry
│
└── schema/
└── article.json ← a JSON Schema, referenced by payload.schema_shortname
Key idea: a folder's meta lives at <subpath>/.dm/meta.folder.json, while its body (the rendering config) is a sibling file named <shortname>.json in the parent directory. This split is exactly what the Folders & Rendering page dissects.
| File | Holds |
|---|---|
.dm/meta.space.json |
Space metadata. |
<subpath>/.dm/meta.folder.json |
Folder metadata for that subpath. |
<subpath>/.dm/<name>/meta.<type>.json |
An entry's meta. |
<subpath>/<name>.json |
An entry's (or folder's) JSON payload body. |
schema/<name>.json |
A JSON Schema definition. |
10. One source of truth, always exportable
DMART keeps a single, authoritative store — and guarantees you can always get your data back out as plain files. Those are two different things, not two competing storage modes.
PostgreSQL — the sole source of truth Every live read and write goes to PostgreSQL (via Npgsql). Narrow per-type tables (users, roles, permissions, spaces) plus a generic entries table, with attachments and histories alongside. ACID, relational integrity, and trigram (pg_trgm + GIN jsonb) full-text search — plus optional pgvector semantic search. There is no filesystem runtime store and no Redis; in-process caches only.
The .dm tree — the export & backup format The human-readable folder tree shown above is how a space is exported, seeded and backed up, not how it is served. dmart export writes a space to a zip in this on-disk layout; import and seed read it back. Human-readable, diff-friendly, and trivially archived with git or tar.
The wire format is the same in both directions: clients always send and receive the same Record envelope, whether it is being persisted to PostgreSQL or round-tripped through a .dm archive. This is what lets DMART promise data longevity with zero vendor lock-in: your data is standard PostgreSQL and can be exported to plain JSON files on disk at any time.