ObjectOS
ConstruirInterfaz

Apps

Bundle objects, views, pages, and dashboards into a branded, navigable shell — and gate exactly who sees what.

Apps

An app is a logical container that bundles objects, views, pages, and dashboards into one cohesive experience. It defines the navigation tree, the branding, and — critically — who gets in.

import { App } from '@objectstack/spec/ui'

export const CrmApp = App.create({
  name: 'crm_app',
  label: 'CRM',
  icon: 'briefcase',
  branding: { primaryColor: '#2563EB' },
  navigation: [
    { id: 'group_sales', type: 'group', label: 'Sales', icon: 'briefcase', children: [
      { id: 'nav_leads',    type: 'object', objectName: 'crm_lead',    label: 'Leads',    icon: 'funnel' },
      { id: 'nav_accounts', type: 'object', objectName: 'crm_account', label: 'Accounts', icon: 'building' },
    ]},
  ],
  requiredPermissions: ['crm_access'],
})

App properties

PropertyTypeRequiredDescription
namestringyesMachine name (snake_case)
labelstringyesDisplay name
iconstringApp icon (Lucide)
description / versionstringMetadata for listings
activebooleanIs the app active (default true)
isDefaultbooleanIs this the default app
navigationNavigationItem[]The navigation tree
brandingAppBrandingprimaryColor, logo, favicon
requiredPermissionsstring[]Who may open the app at all
homePageIdstringNav item id to use as the landing page
mobileNavigationobjectMobile-specific navigation

The navigation tree supports eight item types. The common five are object, dashboard, page, url, and group; the spec also defines report, action, and component items.

typeLands onKey config
objectAn object's list viewobjectName, plus optional targeting (below)
dashboardA dashboarddashboardName
pageA custom pagepageName, optional params
urlAn external URLurl, target: '_blank'
groupCollapsible sectionchildren, expanded

Every item must declare a unique id (lowercase snake_case) — it's what homePageId and mobileNavigation.bottomNavItems reference. Shared properties: label, icon, order, badge, plus the gating trio below.

Targeting an object entry

Three optional fields refine where an object entry lands, with precedence recordIdfiltersviewName:

  • viewName — anchor the entry to a named list view.
  • recordId — deep-link to a single record ("My Profile"); supports {current_user_id} / {current_org_id} template variables.
  • filters — a one-off parameterized slice: the entry lands on the bare data surface with each condition as a removable URL filter chip. Great for "assigned to me" links without authoring a view. Not a security feature — row-level permissions still decide what's shown.
{ id: 'nav_my_open', type: 'object', label: 'My Open Deals', objectName: 'opportunity',
  filters: { owner_id: '{current_user_id}', status: 'open' }, icon: 'user-check' }

Mobile navigation

mobileNavigation: {
  mode: 'bottom_nav',                    // 'drawer' (default) | 'bottom_nav' | 'hamburger'
  bottomNavItems: ['nav_home', 'nav_accounts', 'nav_contacts'],  // nav item ids, max 5
}

Gating apps by audience

The same data serves two very different audiences: builders who design the schema, and end users who just enter and read data. Separate those surfaces by default — don't rely on each admin to hide things by hand.

AudienceSurfaceGated by
End user (consumer)A curated app → page / view entriesApp.requiredPermissions, nav-item gating
Builder / adminSetup / Studio, raw object tablesCapabilities: setup.access, studio.access, manage_metadata

The built-in permission sets already encode this split: member_default and viewer_readonly do not carry studio.access / manage_metadata, so builder surfaces are invisible to them; admin_full_access and organization_admin do.

Each nav item supports three independent gates:

GateTypeHides the item unless…
requiredPermissionsstring[]The user holds the RBAC capability (e.g. manage_metadata)
visibleCEL expressionThe predicate evaluates true (e.g. 'org_admin' in current_user.positions)
requiresObject / requiresServicestringThe named object / kernel service is installed
navigation: [
  { id: 'nav_contacts', type: 'object', label: 'Contacts', objectName: 'showcase_contact' },
  // Builder-only entry — consumers never render it:
  { id: 'nav_designer', type: 'component', label: 'Object Designer',
    componentRef: 'metadata:resource', params: { type: 'object' },
    requiredPermissions: ['manage_metadata'] },
]

Hide, don't disable. A disabled-but-visible builder entry is still noise. Gated nav items are not rendered for users who lack the capability — no greyed-out chrome to confuse end users.

Two more habits keep consumer surfaces clean:

  • Hand end users a page, not the raw table. A page entry lets you curate exactly what's exposed — selected columns, fixed visualization, only the filters and actions you enabled. An object entry gives the permissive grid: switchable views, personal views, the full toolbar.
  • Curate the data surface itself. Prefer a curated view over granting raw object read and dropping users onto a 40-column grid.

Action gates are dual-surface: the UI hides or disables the button and the server rejects the call — there is no "UI-gated but server-open" footgun. See Actions.

The Setup app — a built-in example

The platform's own administration UI, the Setup app, is itself rendered from the same app metadata protocol — navigation, pages, and gating declared as data, drawn by the same renderer as your apps. It's gated exactly the way your builder surfaces should be: behind the setup.access capability that consumer permission sets don't carry. If you want proof that apps-as-metadata scales, you're already using it.

Anti-patterns

  • Making every end user a builder-grade collaborator and hiding tabs one by one. Make the consumer/builder split the default.
  • Disabling builder entries instead of hiding them. Visible-but-dead chrome still confuses.
  • Granting raw object read when a curated page would expose exactly what end users need.

Where to go next

PageWhy
ViewsWhat object nav entries land on
PagesCurated page entries for end users
DashboardsWhat dashboard entries render
ActionsButtons and their dual-surface gates
PermissionsThe permission sets behind requiredPermissions
Interface overviewHow all the pieces compose

On this page