# Data model

URL: https://developers.uidu.org/docs/foundations/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](https://developers.uidu.org/docs/domains/events.md) 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** (`kind` such as `string`, `number`,
  `date`, `singleSelect`, `member`; a stable `shortname`).
* **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](https://developers.uidu.org/docs/domains/cms.md) for reading sites and [Models](https://developers.uidu.org/docs/domains/models.md) for your own types.

<Callout title="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.
</Callout>

### 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 Field
```

Projects 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:

```tsx
<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`) |

<Callout type="warn" title="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.
</Callout>

## 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>`](https://developers.uidu.org/docs/tools/react/components.md):

```tsx
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:

```tsx
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`](https://developers.uidu.org/docs/domains/jobs.md) 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](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md) covers reading everything.
* A field value's `content` is always `{ value: … }` — same page.
* Functions throw `UiduError` on transport and GraphQL failures; writes report validation problems in
  `errors` instead — [Errors](https://developers.uidu.org/docs/foundations/errors.md) and [Writes](https://developers.uidu.org/docs/foundations/writes.md).

<Callout type="info" title="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.
</Callout>

## API at a glance

Every `@uidu/client` function in **Client & utilities**, generated from the source — the [full reference](https://developers.uidu.org/docs/reference/client.md#core) has the rest of the SDK.

| Function                                                                                                                  | Kind   | Auth | CLI | Description                                                                                                                                       |
| ------------------------------------------------------------------------------------------------------------------------- | ------ | ---- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createClient`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts)                              | helper | —    | —   | Build a `UiduClient`; it needs a `workspace` or an `endpoint`, and authenticates with `publicToken`, `apiKey` or `getToken`.                      |
| [`fromBridge`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/client.ts)                                | 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`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/fieldValues.ts)     | 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`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/fieldValues.ts) | helper | —    | —   | Normalize a standalone `fieldValuesAttributes` array. Exported for callers that assemble one by hand outside an SDK mutation.                     |
| [`paginate`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/utils/paginate.ts)                          | read   | —    | —   | Walks every page of a Relay-style connection and returns a flat array of nodes.                                                                   |

**Types:** `PaginateOptions`, `UiduClientConfig`, `UiduErrorOptions`
