---
title: Portal
description: Portal protocol schemas
---

{/* ⚠️  AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */}

@module ui/portal

Portal Protocol — Metadata-driven external-user UI projection.

A Portal is **not** a new application or permission model. It is a

declarative projection of the existing app / view / action surface,

scoped to a route prefix, a set of profiles, and an optional anonymous

entry surface.

Five invariants this schema preserves:

1. Zero business code — layout is an enum + plugin id; theme is tokens;

navigation is references to existing metadata.

2. Data plane is untouched — portal cannot declare objects, fields,

flows, or permissions. Data API (`/api/v1/data/...`) is unaware of

portals.

3. Portal ≠ permission boundary. The Profile is. Portals only narrow

the UI projection; hiding a view in `navigation` is UX, not security.

4. Stackable — the same user/profile can be admitted by multiple

portals. Routing or a picker decides which one is rendered.

5. Template-first — a template author ships `customer.portal.ts` and

the platform guarantees the rendering shell.

Architectural reach (consumer guidance, not part of the schema):

- Dispatcher / HonoServer: at boot, enumerate portals and register

`/<routePrefix>/*` route families with a per-portal auth scope.

- Auth middleware: admit the request if `profile ∈ portal.profiles`,

or it matches `anonymousEntry.routes[*]`.

- objectui LayoutDispatcher: select shell from `layout`.

- objectui NavigationBuilder: render `navigation` (not the all-apps

grid).

- objectui ThemeProvider: inject `theme` as CSS variables.

See framework issue

https://github.com/objectstack-ai/framework/issues/1294

for the design rationale.

<Callout type="info">
**Source:** `packages/spec/src/ui/portal.zod.ts`
</Callout>

## TypeScript Usage

```typescript
import { Portal, PortalActionNavItem, PortalAnonymousEntry, PortalAnonymousRoute, PortalAuthMode, PortalDashboardNavItem, PortalLayout, PortalNavItem, PortalRateLimit, PortalSeo, PortalTheme, PortalUrlNavItem, PortalViewNavItem } from '@objectstack/spec/ui';
import type { Portal, PortalActionNavItem, PortalAnonymousEntry, PortalAnonymousRoute, PortalAuthMode, PortalDashboardNavItem, PortalLayout, PortalNavItem, PortalRateLimit, PortalSeo, PortalTheme, PortalUrlNavItem, PortalViewNavItem } from '@objectstack/spec/ui';

// Validate data
const result = Portal.parse(data);
```

---

## Portal

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **kind** | `string` | ✅ | Metadata kind discriminator. |
| **id** | `string` | ✅ | Portal unique machine name (lowercase snake_case). |
| **label** | `string` | ✅ | Portal display label. |
| **description** | `string` | optional | Display label (plain string; i18n keys are auto-generated by the framework) |
| **routePrefix** | `string` | ✅ | Root URL path for the portal (must start with "/"). |
| **domain** | `string` | optional | Optional vanity domain (e.g. "support.acme.com"). |
| **layout** | `string \| string \| string \| string` | ✅ | Shell layout for the portal. |
| **theme** | `Object` | optional | Theme tokens. |
| **locale** | `string \| string` | ✅ | Locale resolution strategy. |
| **seo** | `Object` | optional |  |
| **authMode** | `string \| string \| string \| string` | ✅ | Authentication mode for the portal. |
| **profiles** | `string[]` | ✅ | Profiles admitted to the portal. |
| **anonymousEntry** | `Object` | optional |  |
| **navigation** | `Object \| Object \| Object \| Object[]` | ✅ | Flat list of portal entry points (references to existing metadata). |
| **defaultRoute** | `Object` | optional | Landing surface when the user hits the portal root. |
| **embeddable** | `boolean` | ✅ |  |
| **allowedEmbedOrigins** | `string[]` | optional |  |
| **active** | `boolean` | ✅ |  |


---

## PortalActionNavItem

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **actionRef** | `string` | ✅ | Reference to an existing action, e.g. "helpdesk_ticket.create". |


---

## PortalAnonymousEntry

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **routes** | `Object[]` | ✅ | List of anonymous-accessible routes. |
| **defaultRateLimit** | `Object` | optional |  |


---

## PortalAnonymousRoute

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **path** | `string` | ✅ | Path within the portal, e.g. "/submit" or "/kb". |
| **viewRef** | `string` | optional | Reference to a public view (read-only). |
| **actionRef** | `string` | optional | Reference to an action to perform anonymously (mutation). |
| **rateLimit** | `Object` | optional | Rate-limit for anonymous traffic on this route. |
| **captcha** | `boolean` | ✅ | Require CAPTCHA / proof-of-work challenge before invoking. |
| **bindIdentityFromField** | `string` | optional | Field name on the action input to use for magic-link identity binding (e.g. "customer_email"). |


---

## PortalAuthMode

### Union Options

This schema accepts one of the following structures:

#### Option 1

Type: `string`

---

#### Option 2

Type: `string`

---

#### Option 3

Type: `string`

---

#### Option 4

Type: `string`

---


---

## PortalDashboardNavItem

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **dashboardName** | `string` | ✅ | Existing dashboard id. |


---

## PortalLayout

### Union Options

This schema accepts one of the following structures:

#### Option 1

Type: `string`

---

#### Option 2

Type: `string`

---

#### Option 3

Type: `string`

---

#### Option 4

Type: `string`

---


---

## PortalNavItem

### Union Options

This schema accepts one of the following structures:

#### Option 1

**Type:** `view`

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **viewRef** | `string` | ✅ | Reference to an existing view, e.g. "helpdesk_ticket.list.my_tickets". |

---

#### Option 2

**Type:** `action`

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **actionRef** | `string` | ✅ | Reference to an existing action, e.g. "helpdesk_ticket.create". |

---

#### Option 3

**Type:** `dashboard`

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **dashboardName** | `string` | ✅ | Existing dashboard id. |

---

#### Option 4

**Type:** `url`

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **url** | `string` | ✅ | Absolute or root-relative URL. |
| **target** | `Enum<'_self' \| '_blank'>` | ✅ |  |

---


---

## PortalRateLimit

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **rule** | `string` | ✅ | Rate-limit rule string, e.g. "5/hour/ip", "100/day/tenant". |
| **scope** | `Enum<'ip' \| 'tenant' \| 'route'>` | ✅ | Counter scope. "ip" buckets per requester; "tenant" per portal owner; "route" global per route. |


---

## PortalSeo

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **title** | `string` | optional | Default `<title>`. |
| **description** | `string` | optional | Default `<meta name="description">`. |
| **openGraphImage** | `string` | optional | Default og:image URL. |
| **robots** | `Enum<'index' \| 'noindex'>` | ✅ |  |


---

## PortalTheme

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **primaryColor** | `string` | optional | Primary brand color (hex, rgb, hsl). Mapped to --portal-primary. |
| **accentColor** | `string` | optional | Accent color used for highlights and CTAs. |
| **backgroundColor** | `string` | optional | Page background color. |
| **surfaceColor** | `string` | optional | Card / surface background color. |
| **textColor** | `string` | optional | Primary text color. |
| **logoUrl** | `string` | optional | Absolute or relative URL to the portal header logo (SVG or PNG). |
| **faviconUrl** | `string` | optional | Absolute or relative URL to the portal favicon. |
| **fontFamily** | `string` | optional | CSS font-family stack for the portal. |
| **customCss** | `string` | optional | OPTIONAL escape hatch: raw CSS appended last. Discouraged; prefer tokens. |


---

## PortalUrlNavItem

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **url** | `string` | ✅ | Absolute or root-relative URL. |
| **target** | `Enum<'_self' \| '_blank'>` | ✅ |  |


---

## PortalViewNavItem

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Unique identifier for this portal nav item (lowercase snake_case). |
| **label** | `string` | ✅ | Display label. |
| **icon** | `string` | optional | Icon name (Lucide). |
| **order** | `number` | optional | Sort order; lower appears first. |
| **badge** | `string \| number` | optional | Optional badge (e.g. unread count). |
| **type** | `string` | ✅ |  |
| **viewRef** | `string` | ✅ | Reference to an existing view, e.g. "helpdesk_ticket.list.my_tickets". |


---

