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.
Related guides
- Layout —
AppShell, the primitives and scrolling - UI components — views, the toolbar and record views
- Navigation state — record sheets and view state in the URL
- Internationalization — message catalogs