Skip to content

Apps

Apps

An app is a folder under src/app/: +app.ts declares it, and each +<page>.page.svelte beside it is one page. A folder may instead hold a +group.ts — a nav group whose apps, or further groups, live inside it. An app’s name is its folder path.

src/app/
├── field_ops_controller/
│   ├── +app.ts
│   ├── +dispatch.page.svelte
│   └── +sites.page.svelte
├── field_ops_contractor/
│   ├── +app.ts
│   └── +jobs.page.svelte
└── <group>/
    ├── +group.ts
    └── <app>/+app.ts …

Declaring an app

// src/app/field_ops_controller/+app.ts (templates/field-operations)
import { app } from '@norbital-ai/bolt';

export default app('field_ops_controller', {
  title: 'app.field_ops_controller.title',
  description: 'Schedule site jobs and dispatch contractors',
  icon: 'lucide:building-2',
  banner: 'app-media/field_ops_controller-banner.webp',
  pages: {
    dispatch: { title: 'app.field_ops_controller.tab_dispatch', icon: 'lucide:kanban' },
    sites: { title: 'app.field_ops_controller.tab_sites', icon: 'lucide:map-pinned' }
  }
});

app('<name>', { … }) takes the title, description and icon of the app card, an optional banner, an audience, and pages — keyed by page file name, in nav order, each with a title, an optional icon, a section that groups it, and kiosk: true for a full-screen page. A page file with no entry is a build error. Titles are catalog keys once the workspace has src/i18n/.

Audience

'members' (the default) shows the app to staff, 'external' to external members such as customer and vendor portal users, and 'all' to both. { public: [policies], challenge?: 'turnstile' } serves its pages to signed-out visitors, who hold exactly those policies and may only submit forms and read published rows.

A member sees an app only when one of their policies lists it — or a group that contains it — in capabilities.apps. apps in +workspace.ts sets the order of apps in the launcher.

Pages

<!-- src/app/field_ops_controller/+sites.page.svelte (templates/field-operations, excerpt) -->
<script lang="ts">
  import { bolt } from '$bolt';
  import { AppShell } from '@norbital-ai/ui/layout';
  import { Table } from '@norbital-ai/ui';

  const t = bolt.t;
</script>

<AppShell
  icon="lucide:map-pinned"
  title={t('app.field_ops_controller.tab_sites')}
  description={t('app.field_ops_controller.sites_description')}
>
  <Table
    of="sites"
    orderBy={{ name: 'asc' }}
    columns={['name', { field: 'client_name', label: t('component.client_tenant') }, 'address']}
  />
</AppShell>

A page is a Svelte 5 component. It imports bolt from $bolt for reads and writes, views from @norbital-ai/ui, and layout from @norbital-ai/ui/layout. Every page is one AppShell, whose title, description and icon render the page’s hero. A page never imports a role module: it opens records through RecordShell and the record sheet.

Groups

src/app/<group>/+group.ts is group('<group>', { label, description?, icon, defaultChild }). A folder holds either an app or a group, never both, and a policy that names a group opens every app inside it.