Portal
Portal protocol schemas
@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:
- Zero business code — layout is an enum + plugin id; theme is tokens;
navigation is references to existing metadata.
- Data plane is untouched — portal cannot declare objects, fields,
flows, or permissions. Data API (/api/v1/data/...) is unaware of
portals.
- Portal ≠ permission boundary. The Profile is. Portals only narrow
the UI projection; hiding a view in navigation is UX, not security.
- Stackable — the same user/profile can be admitted by multiple
portals. Routing or a picker decides which one is rendered.
- Template-first — a template author ships
customer.portal.tsand
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
themeas CSS variables.
See framework issue
https://github.com/objectstack-ai/framework/issues/1294
for the design rationale.
Source: packages/spec/src/ui/portal.zod.ts
TypeScript Usage
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". |