This document defines the JSON specification format consumed by the ODS React Web framework. Both the React and Flutter frameworks implement the same spec format.
{
"appName": "My App",
"startPage": "homePage",
"logo": "https://...",
"favicon": "https://...",
"auth": { ... },
"menu": [ ... ],
"pages": { ... },
"dataSources": { ... },
"theme": { ... },
"settings": { ... },
"help": { ... },
"tour": [ ... ]
}
| Field | Type | Description |
|---|---|---|
appName |
string | Display name of the application |
startPage |
string or object | Default landing page ID, or role-based map |
pages |
object | Map of page IDs to page definitions |
| Field | Type | Default | Description |
|---|---|---|---|
auth |
object | { multiUser: false } |
Authentication configuration |
menu |
array | [] |
Navigation menu items |
dataSources |
object | {} |
Data source definitions |
theme |
object | defaults (indigo/system) |
Visual theme + customizations (see ADR-0002) |
logo |
string | none | URL to the app logo shown in sidebar/drawer |
favicon |
string | none | URL to the browser-tab icon |
appIcon |
string | none | Optional emoji or icon identifier alongside the app name |
settings |
object | {} |
User-configurable app settings |
help |
object | null | In-app help content |
tour |
array | [] |
Guided tour steps |
Can be a simple string or a role-based object:
// Simple
"startPage": "homePage"
// Role-based
"startPage": {
"default": "userDashboard",
"admin": "adminDashboard",
"manager": "managerView"
}
The default key is used as fallback when no role matches.
Each page has a title and an array of components:
"pages": {
"homePage": {
"title": "Home",
"content": [
{ "component": "text", "content": "Welcome!" },
{ "component": "list", "dataSource": "items", ... }
],
"roles": ["admin"]
}
}
| Field | Type | Required | Description |
|---|---|---|---|
title |
string | yes | Page heading |
content |
array | yes | Array of component objects |
roles |
string[] | no | Restrict page to specific roles |
All components share these base fields:
| Field | Type | Description |
|---|---|---|
component |
string | Component type identifier |
styleHint |
object | Optional styling hints |
visibleWhen |
object | Conditional visibility rule |
visible |
string | Expression-based visibility |
roles |
string[] | Role-based visibility |
Static or dynamic text content.
{
"component": "text",
"content": "Total tasks: {COUNT(tasks)}",
"format": "markdown"
}
| Field | Default | Values |
|---|---|---|
content |
— | Literal text. Supports aggregate references like {COUNT(ds)}, {SUM(ds, field)} |
format |
"plain" |
"plain", "markdown" |
Data entry form with typed fields.
{
"component": "form",
"id": "addTaskForm",
"fields": [
{ "name": "title", "label": "Title", "type": "text", "required": true },
{ "name": "priority", "label": "Priority", "type": "select", "options": ["High", "Medium", "Low"] },
{ "name": "dueDate", "label": "Due", "type": "date", "default": "+7d" }
],
"recordSource": "tasksReader"
}
| Field | Description |
|---|---|
id |
Unique form identifier (referenced by actions) |
fields |
Array of field definitions |
recordSource |
Optional: pre-fill from a data source with record cursor |
| Type | Description | Extra Properties |
|---|---|---|
text |
Single-line text | placeholder, minLength, maxLength |
multiline |
Multi-line textarea | placeholder |
number |
Numeric input | min, max |
date |
Date picker | default: "+7d", "NOW" |
select |
Dropdown | options: string array |
checkbox |
Toggle | — |
computed |
Read-only calculated | formula: e.g., "{qty} * {price}" |
hidden |
Hidden field | default |
user |
User picker (multi-user) | — |
| Property | Type | Description |
|---|---|---|
name |
string | Field identifier |
label |
string | Display label |
type |
string | Field type (see above) |
required |
boolean | Validation: must have value |
default |
string | Default value |
placeholder |
string | Input placeholder text |
options |
string[] | Options for select fields |
formula |
string | Expression for computed fields |
visibleWhen |
object | Conditional visibility |
validation |
object | Custom validation rules |
Tabular data display with sorting, filtering, and actions.
{
"component": "list",
"dataSource": "tasksReader",
"searchable": true,
"columns": [
{ "header": "Task", "field": "title", "sortable": true },
{ "header": "Status", "field": "status", "filterable": true, "colorMap": { "Done": "green" } }
],
"rowActions": [
{ "label": "Delete", "action": "delete", "dataSource": "tasksStore", "matchField": "_id", "confirm": "Delete this task?" }
],
"defaultSort": { "field": "dueDate", "direction": "desc" }
}
| Field | Default | Description |
|---|---|---|
dataSource |
— | Data source ID to query |
columns |
[] |
Column definitions |
rowActions |
[] |
Per-row action buttons |
searchable |
false |
Show search input |
displayAs |
"table" |
"table" or "cards" |
defaultSort |
none | Default sort field and direction |
summary |
[] |
Footer summary rules (SUM, COUNT, AVG) |
onRowTap |
none | Navigate on row click |
Action trigger with chained actions.
{
"component": "button",
"label": "Save",
"onClick": [
{ "action": "submit", "dataSource": "tasksStore", "target": "addTaskForm" },
{ "action": "showMessage", "message": "Saved!" },
{ "action": "navigate", "target": "listPage" }
]
}
Data visualization.
{
"component": "chart",
"dataSource": "salesReader",
"chartType": "bar",
"labelField": "month",
"valueField": "revenue",
"aggregate": "sum"
}
| Field | Default | Values |
|---|---|---|
chartType |
"bar" |
"bar", "line", "pie" |
aggregate |
auto | "count", "sum", "avg" |
KPI / metric card.
{
"component": "summary",
"label": "Total Tasks",
"value": "{COUNT(tasksReader)}",
"icon": "task"
}
Single-record read-only view.
{
"component": "detail",
"dataSource": "tasksReader",
"fields": ["title", "status", "dueDate"],
"labels": { "dueDate": "Due Date" },
"fromForm": "editForm"
}
Tabbed layout container.
{
"component": "tabs",
"tabs": [
{ "label": "Overview", "content": [ ... ] },
{ "label": "Details", "content": [ ... ] }
]
}
Board layout with drag-drop between columns.
{
"component": "kanban",
"dataSource": "tasksReader",
"statusField": "status",
"titleField": "title",
"cardFields": ["assignee", "priority", "dueDate"],
"searchable": true,
"rowActions": [ ... ]
}
The statusField must reference a select field with defined options — these become the column headers.
Actions are triggered by buttons and row actions. They execute sequentially.
| Action | Description | Key Fields |
|---|---|---|
navigate |
Go to a page | target: page ID |
submit |
Insert form data | target: form ID, dataSource |
update |
Update existing record | target: form ID or row ID, dataSource, matchField |
delete |
Delete a record | dataSource, matchField |
showMessage |
Show toast message | message, optional level (info | success | warning | error, default info) |
firstRecord |
Navigate to first record | — |
nextRecord |
Navigate to next record | — |
previousRecord |
Navigate to previous record | — |
lastRecord |
Navigate to last record | — |
| Property | Type | Description |
|---|---|---|
action |
string | Action type |
target |
string | Form ID, page ID, or row ID |
dataSource |
string | Data source to operate on |
matchField |
string | Field to match for update/delete |
withData |
object | Direct data for update (no form needed) |
confirm |
string | Confirmation prompt before executing |
computedFields |
array | Fields to compute at submit time |
onEnd |
object | Chained action after completion |
preserveFields |
string[] | Fields to keep after form clear |
cascade |
object | Cascade rename config |
"dataSources": {
"tasksStore": {
"url": "local://tasks",
"method": "POST",
"fields": [ ... ]
},
"tasksReader": {
"url": "local://tasks",
"method": "GET"
}
}
| Field | Description |
|---|---|
url |
local://tableName for local storage |
method |
GET (read), POST (insert), PUT (update) |
fields |
Field definitions (schema for auto-creation) |
seedData |
Initial data rows |
ownership |
Row-level security config |
The framework automatically manages these fields on every record. Specs should not define fields with these names.
| Field | Type | Description |
|---|---|---|
_id |
string | Unique record identifier. Generated automatically on insert. Opaque — specs should never hardcode _id values. Always reference dynamically via matchField. Format is a 15-character alphanumeric string. |
_createdAt |
string | ISO 8601 timestamp set at insert time. |
_owner |
string | Owner identifier (set when ownership is enabled). |
"ownership": {
"enabled": true,
"ownerField": "_owner",
"adminOverride": true
}
"auth": {
"multiUser": true,
"selfRegistration": true,
"defaultRole": "user",
"multiUserOnly": false
}
| Field | Default | Description |
|---|---|---|
multiUser |
false |
Enable multi-user mode |
selfRegistration |
false |
Allow users to register |
defaultRole |
"user" |
Role assigned to new users |
multiUserOnly |
false |
Disable guest access entirely |
"menu": [
{ "label": "Dashboard", "mapsTo": "dashPage" },
{ "label": "Admin Panel", "mapsTo": "adminPage", "roles": ["admin"] }
]
Visual theme + customizations. A base theme picks colors and typography from the catalog; overrides adjusts any token (color, font, header style, etc.) on top of it. App identity (logo, favicon, appIcon) lives at the top-level of the spec, not here. See ADR-0002.
"theme": {
"base": "nord",
"mode": "dark",
"headerStyle": "solid",
"overrides": {
"primary": "oklch(50% 0.2 260)",
"fontSans": "Inter"
}
}
| Field | Default | Values |
|---|---|---|
base |
"indigo" |
Any of 35+ built-in themes |
mode |
"system" |
"light", "dark", "system" |
headerStyle |
"light" |
"light", "solid", "transparent" |
overrides |
{} |
Per-token overrides — colors, fonts (fontSans/fontSerif/fontMono), radius, etc. |
Style hints are an open-ended bag of rendering suggestions:
"styleHint": {
"variant": "heading",
"emphasis": "primary",
"align": "center",
"color": "info",
"icon": "star",
"size": "large",
"density": "compact",
"elevation": 2
}
"visibleWhen": {
"form": "myForm",
"field": "category",
"equals": "premium"
}
"visibleWhen": {
"source": "tasksReader",
"countEquals": 0
}
"help": {
"overview": "Welcome to the app...",
"pages": {
"homePage": "This page shows your tasks."
}
},
"tour": [
{ "title": "Welcome", "content": "Let's take a quick tour.", "page": "homePage" }
]
User-configurable app settings:
"settings": {
"currency": {
"label": "Currency Symbol",
"type": "select",
"default": "$",
"options": ["$", "EUR", "GBP", "JPY"]
}
}
Used in computed fields, summary values, and text content.
{fieldName} resolves to the field’s current value{quantity} * {unitPrice} — +, -, *, /, parentheses, decimals, negatives{COUNT(dataSource)}, {SUM(dataSource, field)}, {AVG(dataSource, field)}, {MIN(dataSource, field)}, {MAX(dataSource, field)}NOW (current date), +7d (relative date; also -3d, +1m, etc.){status} == "done" ? "Complete" : "Pending" — supports operators ==, !=, >, <, >=, <=
== / !=; numeric for >, <, >=, <={field} interpolation directly — only aggregate references. Use a computed field for field-based display.