uidu SDK
Paths

Custom app

Build an app that uidu shows inside a Space, acting as the signed-in member — from the template to your own data in Models.

A custom app is a web app you host yourself that uidu shows inside a Space (or the workspace) in a sandboxed iframe. uidu hands it a short-lived session for the member looking at it, so the app reads and writes as that person — no login screen, no token to configure, nothing secret in the browser.

By the end of this path you'll have an app that connects to uidu, knows who is looking, reads the workspace's goals, and keeps its own records in a Model.

Scaffold from the template

npm create uidu-app@latest my-app -- -t custom-app

You get a Next.js app with <UiduAppProvider> already wired, a screen for when it's opened outside uidu, the page's theme and accent colour applied, public/uidu.app.json to declare the app's Models, and a Content-Security-Policy: frame-ancestors header so only uidu can frame it. See create-uidu-app.

Connect to the page framing you

Everything that touches member data sits under <UiduAppProvider>, in client components:

// src/components/app-provider.tsx — rendered by app/layout.tsx around {children}
'use client';
import { UiduAppProvider } from '@uidu/react';
import { DEFAULT_HOST_ORIGINS } from '@uidu/app-bridge';

export function AppProvider({ children }: { children: React.ReactNode }) {
  // https://*.uidu.org by default; spread it to add a local uidu or a custom domain
  return <UiduAppProvider hostOrigins={[...DEFAULT_HOST_ORIGINS]}>{children}</UiduAppProvider>;
}

The session only exists in the browser, after the page loads: a Server Component or a server action never has it, so pages that show member data are 'use client'. Why, and the other rules of the session: Authentication.

Know who is looking

useUiduApp() gives you the connection state, a session-backed client, and the context uidu sent:

'use client';
import { useUiduApp } from '@uidu/react';

export function Hello() {
  const app = useUiduApp(); // 'connecting' | 'ready' | 'error'

  if (app.status === 'connecting') return <p>Connecting…</p>;
  if (app.status === 'error') return <p>Open this app from uidu ({app.error.code})</p>;

  const { user, space, locale, theme } = app.context;
  return <p>Hi {user?.name} — {space?.name ?? 'whole workspace'}</p>;
}

context also carries workspaceApp (the installed app — the owner of its Models) and accent, the page's colour to set as --primary. Every field and state: App bridge.

Read what the workspace already has

A workspace isn't empty, and the session can read some of it. Today that's the workspace's goals — those of the workspace and of the Space the app sits in:

import { listGoals, isGoalBehind } from '@uidu/client';

const goals = await listGoals(app.client);
const behind = goals.filter((g) => !g.parentId && isGoalBehind(g, { tolerance: 0.1 }));

Everything else — contacts, deals, people, events, tasks — is refused to a session for now. When the data exists in uidu but the app can't reach it yet, say so to the person rather than copying it into a Model: the copy drifts from the real records.

Keep your own data in a Model

For data uidu has no place for — a room booking, a checklist — define a Model and store items in it. They stay searchable and permissioned inside uidu.

import { createModelItem, ensureModel, listModelItems, toFieldValuesAttributes } from '@uidu/client';

const model = await ensureModel(app.client, {
  workspaceAppId: app.context.workspaceApp.id,
  name: 'Booking',
  fields: [{ shortname: 'room', name: 'Room', kind: 'string' }],
});

const created = await createModelItem(app.client, {
  input: {
    attributes: {
      modelId: model.id,
      fieldValuesAttributes: toFieldValuesAttributes(model, { room: 'Blu' }),
    },
  },
});

const items = await listModelItems(app.client, { modelId: model.id }); // item.fieldValuesByShortname.room

Two things that bite

Call ensureModel once per app instance (memoize the promise): two concurrent calls each create the model. And listModelItems reads uidu's search index, which catches up a moment after a write — after createModelItem / deleteModelItem, update your list from the mutation payload instead of listing again.

Handle being opened outside uidu

Opened directly in a tab, the app has no host to connect to: useUiduApp() ends in 'error' with NOT_EMBEDDED. The template shows a "open this from uidu" screen; the demo falls back to localStorage. Don't use cookies or localStorage for shared state inside uidu — in a cross-origin iframe cookies are third-party (Safari drops them) and localStorage is one browser only.

Ship it

Deploy the app anywhere that serves HTTPS, then add it to a Space in uidu. Keep two lists in step: the hostOrigins you give the bridge and the frame-ancestors header — the template builds both from https://*.uidu.org plus NEXT_PUBLIC_UIDU_HOST_ORIGINS. If uidu runs on your own domain, add it to that variable.

Where to go next

  • App bridge — connect(), error codes, origins, the protocol, fromBridge.
  • Models — fields, field options, updating and deleting items.
  • Goals — values stored ×100, progress helpers.
  • Authentication — how the session differs from the other two tokens.

On this page