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

<h1>{event.name ?? 'Untitled event'}</h1>

A few entities have no name field (they're transactional or derived):

EntityWhat to show instead
Booking${kind} #${number} or contact.name
Enrollmentcourse.name + state
EmailCampaignsubject
Employeeuser.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 content is always { value: … } — same page.
  • Functions throw UiduError on transport and GraphQL failures; writes report validation problems in errors instead — 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.

FunctionKindAuthCLIDescription
createClienthelper——Build a UiduClient; it needs a workspace or an endpoint, and authenticates with publicToken, apiKey or getToken.
fromBridgehelper——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.
normalizeFieldValueContenthelper——Wrap a content payload in { value } unless it already carries a value key. Returns the input untouched when there is nothing to do.
normalizeFieldValuesAttributeshelper——Normalize a standalone fieldValuesAttributes array. Exported for callers that assemble one by hand outside an SDK mutation.
paginateread——Walks every page of a Relay-style connection and returns a flat array of nodes.

Types: PaginateOptions, UiduClientConfig, UiduErrorOptions

On this page