# Custom app

URL: https://developers.uidu.org/docs/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

```bash
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](https://developers.uidu.org/docs/tools/create-uidu-app.md).

### Connect to the page framing you

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

```tsx
// 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](https://developers.uidu.org/docs/foundations/authentication.md#session--the-signed-in-member-inside-uidu).

### Know who is looking

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

```tsx
'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](https://developers.uidu.org/docs/tools/app-bridge.md).

### 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](https://developers.uidu.org/docs/domains/goals.md) — those of the workspace and of the Space the app sits in:

```ts
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](https://developers.uidu.org/docs/domains/models.md)
and store items in it. They stay searchable and permissioned inside uidu.

```ts
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
```

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

### 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](https://github.com/uidu-org/api.js/tree/main/apps/demo/src/app/custom-app) 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](https://developers.uidu.org/docs/tools/app-bridge.md) — `connect()`, error codes, origins, the protocol, `fromBridge`.
* [Models](https://developers.uidu.org/docs/domains/models.md) — fields, field options, updating and deleting items.
* [Goals](https://developers.uidu.org/docs/domains/goals.md) — values stored ×100, progress helpers.
* [Authentication](https://developers.uidu.org/docs/foundations/authentication.md) — how the session differs from the other two tokens.
