ObjectStackObjectStack

Audience-based interfaces

The same data serves different audiences. Give end users a curated app/page and keep builder surfaces — Studio, raw object tables, automation config — out of their view. Separate consumer and builder paths by default.

Audience-based interfaces

Scenario

The same object is touched by two very different audiences: builders who design the schema and end users who just enter and read data. How do I give each the right surface — without end users seeing (and being confused by) the builder chrome?

Separate the consumer surface from the builder surface by default — don't rely on each admin to hide things by hand. ObjectStack has two first-class run modes (ADR-0047); navigation type decides which a user gets.

AudienceSurfaceGated by
End user (consumer)a curated Apppage / view (interface mode)App.requiredPermissions, permission-set tabPermissions, nav-item gating
Builder / adminSetup / Studio, raw object tables (data mode)capabilities: 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 Studio and schema-design surfaces are invisible to them; admin_full_access and organization_admin do (see default-permission-sets.ts).

1. Gate the app and its navigation

defineApp({
  name: 'crm',
  label: 'CRM',
  requiredPermissions: ['crm.access'],   // who may open the app at all
  navigation: [
    { id: 'nav_contacts', type: 'object', label: 'Contacts', objectName: 'showcase_contact' },
    // Builder-only entries are gated so consumers never render them:
    { id: 'nav_designer', type: 'component', label: 'Object Designer',
      componentRef: 'metadata:resource', params: { type: 'object' },
      requiredPermissions: ['manage_metadata'] },
  ],
});

Each nav item supports three independent gates (app.zod):

  • requiredPermissions — RBAC capability (e.g. manage_metadata).
  • visible — a CEL predicate (e.g. P\'org_admin' in current_user.positions``).
  • requiresObject / requiresService — hide unless a runtime object/service is installed.

Hide, don't disable. A disabled-but-visible builder entry is still noise and still confuses end users. Gated nav items are not rendered for users who lack the capability.

2. Hand end users a page, not the raw table

In data mode (navigation → object) users get the permissive grid: switchable views, personal views, a full toolbar. In interface mode (navigation → page) the author curates exactly what's exposed — selected columns, fixed visualisation, only the filters and actions you enabled. For end users, prefer interface-mode pages. ADR-0047's default is deliberately asymmetric: data mode is open, interface mode is closed until the author opens it.

3. Curate the data surface itself

Use a curated view (selected fields, progressive disclosure) rather than granting read on the raw object and dropping users onto a 40-column grid.

Why

This is the lesson from Airtable's surfaces: the Automations tab is visible to every base collaborator, even read-only — so end users see builder chrome they can't use and don't understand. ObjectStack avoids that by making "consumer vs builder" a capability boundary that the built-in permission sets already draw, plus per-nav gating that omits (not merely disables) what a user can't use. Action gates are dual-surface (ADR-0066 D4): the ActionRunner hides/disables an action in the UI and the server rejects the call — no "UI-gated but server-open" footgun.

Runnable example

Anti-patterns

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

See also

On this page