Data model
How uidu's data is shaped — workspace, Sites and Models, the platform's own entities — and the conventions that repeat across all of them.
Internalize this once and every domain page reads the same way.
The workspace
Everything lives in a workspace — one per organization, served at https://{workspace}.uidu.org,
with its own GraphQL endpoint. Tokens are workspace-scoped. Inside it there are two kinds of data:
- The platform's own entities —
Event,Story,Form,Course,DonationCampaign,Contact,Employee,Goal… Each uidu app (Events, CRM, People…) owns some, they hang directly off the workspace, and each domain page documents one family. - Models — record types you define yourself, with typed Fields. They hold whatever uidu has no place for, and they are also what the CMS is built from.
Sites and Models
Workspace
├── Site a website: domain, settings
│ ├── page-kind Model "Page", "Landing" — a page type, with its fields
│ │ └── ModelItem one page: slug, field values, ordered blocks
│ └── block-kind Model "Hero", "Feature grid" — a block type
│ └── ModelItem one block placed on a page
└── Model your own record type ("Booking", "Checklist")
├── Field name, kind, shortname
└── ModelItem one record
└── FieldValue one field's value: content = { value: … }- Model — a record type: a name and a list of Fields (
kindsuch asstring,number,date,singleSelect,member; a stableshortname). - ModelItem — one record of a Model. Its values are FieldValues, read back keyed by shortname
(
item.fieldValuesByShortname.room). - Site — the CMS. A site's page types and block types are Models; its pages and blocks are their items. That's why rendering a page and reading your own records look alike. See CMS for reading sites and Models for your own types.
Read what the workspace has before creating a Model
A workspace is not empty: goals, contacts, deals, people, events are already there. Create a Model only for data uidu has no place for — copying existing records into one gives the organization a second copy that drifts from the real one.
Legacy CMS: Projects
Sites already deployed on the older CMS keep working:
Workspace
└── Project "marketing site", "help center"
└── Page identified by slug
└── PageBlock one section, typed by its templateBlock
└── FieldValue one piece of data, typed by its FieldProjects can be migrated to Sites from the CMS app. New sites should start on Sites.
Naming: use name
Display names come from the entity's name field. It's user-set and may be null on drafts — render
a fallback when that happens:
<h1>{event.name ?? 'Untitled event'}</h1>A few entities have no name field (they're transactional or derived):
| Entity | What to show instead |
|---|---|
Booking | ${kind} #${number} or contact.name |
Enrollment | course.name + state |
EmailCampaign | subject |
Employee | user.name (or user.firstName + lastName) |
Don't use humanName
The schema exposes a humanName field on most types. It's not a display name — it's the Rails
model_name.human (the humanized class label like "Booking" or "Calendar event"). Useful for
__typename-style disambiguation, never for the title of a record.
Rich text: body is Tiptap JSON
Rich-text fields (Event.body, Call.body, Course.body, CMS block bodies, CalendarEvent.body,
Lecture.body, KbArticle.body) are stored as Tiptap/ProseMirror JSON — a tree of
{ type, content, marks } nodes, typed JSON in the schema. Render them with
<RichText>:
import { RichText } from '@uidu/react';
<RichText doc={event.body} />Money: amounts are in cents
Money values (DonationCampaign.goal, Donation.amount, Deal.amount, product prices, booking
totals) are integers in the smallest unit of the currency — cents for EUR/USD. Divide by 100 to
display:
const eur = (campaign.goal ?? 0) / 100;
<span>€{eur.toLocaleString()}</span>The exception is a job's salary range (Call.salaryMin / salaryMax), which is in whole units —
formatSalaryRange formats it.
Lists, field values, errors
Three more rules apply everywhere and have their own pages:
list*functions return one page, flattened — Pagination & field values covers reading everything.- A field value's
contentis always{ value: … }— same page. - Functions throw
UiduErroron transport and GraphQL failures; writes report validation problems inerrorsinstead — Errors and Writes.
Schema-typed everywhere
Arguments and return values are typed from the live schema via codegen. If a field is nullable in the schema, it's nullable in TypeScript.
API at a glance
Every @uidu/client function in Client & utilities, generated from the source — the full reference has the rest of the SDK.
| Function | Kind | Auth | CLI | Description |
|---|---|---|---|---|
createClient | helper | — | — | Build a UiduClient; it needs a workspace or an endpoint, and authenticates with publicToken, apiKey or getToken. |
fromBridge | helper | — | — | Client config for a custom app, from a connected @uidu/app-bridge: the bridge's GraphQL endpoint, with its session token asked for per request. |
normalizeFieldValueContent | helper | — | — | Wrap a content payload in { value } unless it already carries a value key. Returns the input untouched when there is nothing to do. |
normalizeFieldValuesAttributes | helper | — | — | Normalize a standalone fieldValuesAttributes array. Exported for callers that assemble one by hand outside an SDK mutation. |
paginate | read | — | — | Walks every page of a Relay-style connection and returns a flat array of nodes. |
Types: PaginateOptions, UiduClientConfig, UiduErrorOptions