<!-- Source: https://docs.squirro.com/en/latest/technical/neo/dev-guide/manifest.html -->
# The Bundle Manifest

> **Warning**
>
> Project Neo is currently in [Technical Preview](../../../a-z/squirro-glossary.md#term-Technical-Preview). Features described in this section may change before general availability.

The manifest is the central configuration file for your bundle. It declares what dashboards your bundle provides, how they appear in the sidebar, and optionally supplies translated strings.

**File:** `src/manifest.ts`

> **Note**
>
> You rarely need to edit this file by hand. The CLI manages it automatically: `neo-ui create dashboard` adds entries, `neo-ui remove dashboard` removes them, and `neo-ui translations init` wires up translations. The reference below covers what the CLI generates and the rare cases where manual adjustments are needed, such as reordering dashboards or changing an icon.

## Full Type Reference

You pass the manifest object to `defineBundleManifest`, which validates it and the brand theme at build time:

```typescript
import { defineBundleManifest } from '@squirro/neo-core';
import type { BundleTheme } from '@squirro/neo-core';

// Shape accepted by defineBundleManifest (exported as the BundleManifest type):
interface BundleManifest {
  /** Unique identifier for this bundle. Used in translations and internal naming. */
  name: string;

  /**
   * Semver of @squirro/neo-core the bundle was built against.
   * Injected automatically at build time by the CLI. You do not set this manually.
   * The host compares it against its own version and logs a warning on mismatch.
   */
  platformVersion: string;

  /** The list of dashboards this bundle provides. */
  dashboards: BundleDashboard[];

  /**
   * Optional translations.
   * Keys without ':' go into the 'ext-{name}' namespace (your own strings).
   * Keys with ':' (for example, 'chat:inputPlaceholder') override the matching host key.
   */
  translations?: Record<string, Record<string, string>>;

  /** Optional display names for locales you add (for example, { es: 'Español' }). Managed by `neo-ui translations add-locale`. */
  localeLabels?: Record<string, string>;

  /** Optional build-time brand theme (colors, logo, favicon). Defined in src/theme.ts. */
  theme?: BundleTheme;

  /** Set true on exactly one bundle per tenant to use its theme as the host default branding. */
  isBrandingSource?: boolean;

  /** Optional sidebar overrides. Reorders and hides core items and dashboards. */
  nav?: BundleNavConfig;
}

interface BundleNavConfig {
  /**
   * IDs appended after all the items that are not listed, in the order given.
   * Items that are not listed keep the default order: chat, then search,
   * followed by the dashboards in manifest order. List every ID for a full custom order.
   */
  order?: string[];

  /** IDs removed from the sidebar, for every viewer. */
  hidden?: string[];
}

interface BundleDashboard {
  /** Unique identifier within this manifest. Doubles as the route ID. */
  id: string;

  /** Display name shown in the sidebar navigation. */
  title: string;

  /** Lucide icon name in kebab-case (for example, 'bar-chart-3'). */
  icon: string;

  /** URL slug. No leading slash, lowercase kebab-case recommended. */
  route: string;

  /** Lazy import returning the dashboard React component. */
  component: () => Promise<{ default: ComponentType<{ projectId: string }> }>;

  /**
   * Optional. Restrict the dashboard to viewers who belong to at least one of these groups.
   * Each entry matches the name or the ID of a group. Omit the field or leave it empty for a
   * dashboard visible to everyone. A convenience for tailoring the interface, not a security
   * boundary: data access is enforced by the Squirro platform.
   */
  allowedGroups?: string[];
}
```

## A Complete Manifest Example

```typescript
import { defineBundleManifest } from '@squirro/neo-core';
import theme from './theme';

declare const NEO_CORE_VERSION: string;

if (import.meta.env.DEV) {
  void import('./dev-tools');
}

export default defineBundleManifest({
  name: 'my-dashboards',
  platformVersion: NEO_CORE_VERSION,
  dashboards: [
    {
      id: 'analytics',
      title: 'Analytics',
      icon: 'bar-chart-3',
      route: 'analytics',
      component: () => import('./dashboards/Analytics'),
    },
    {
      id: 'reports',
      title: 'Monthly Reports',
      icon: 'file-text',
      route: 'reports',
      component: () => import('./dashboards/Reports'),
    },
  ],
  theme,
  isBrandingSource: false,
});
```

## Dashboard Fields

### The id Field

A string that uniquely identifies the dashboard within your bundle. By default, the CLI sets it to the same value as `route`. It is used internally and does not need to be changed.

### The title Field

The text label shown in the sidebar navigation. Keep it short. The sidebar has limited horizontal space.

### The icon Field

A Lucide icon name in kebab-case. Browse all available icons at [lucide.dev/icons](https://lucide.dev/icons).

Common choices:

| Icon name | Use case |
| --- | --- |
| `layout-dashboard` | General dashboard |
| `bar-chart-3` | Analytics and charts |
| `file-text` | Reports and documents |
| `users` | People and accounts |
| `search` | Search results |
| `settings` | Configuration |
| `database` | Data and storage |
| `activity` | Activity and monitoring |
| `map` | Geographic data |
| `calendar` | Scheduling and timeline |

If the icon name is invalid or unknown, the host renders nothing in that slot. No crash occurs.

### The route Field

The URL path segment for this dashboard. The full URL takes the form:

```text
/:projectId/dashboard/{route}
```

Rules:

- Must be unique across all dashboards in the manifest.
- No leading or trailing slashes.
- Lowercase kebab-case is strongly recommended.
- Treat routes as permanent. Renaming a route breaks saved bookmarks and deep links.

### The component Field

A function returning a dynamic import. That approach splits the dashboard bundle so it is only fetched when the user navigates to that dashboard, not on application startup.

```typescript
component: () => import('./dashboards/Analytics'),
```

The import must resolve to a file with a default export matching the dashboard component signature. For more information, see the [Building Dashboard Components](building-dashboards.md#neo-extensions-building-dashboards) page.

### The allowedGroups Field

An optional list of user groups. When it is set, the dashboard is shown only to viewers who belong to at least one of the listed groups. Each entry matches either the name or the ID of a group:

```typescript
allowedGroups: ['Executives', 'Finance'],
```

Omit the field, or leave the array empty, to show the dashboard to everyone. To list the groups of your instance with their names and IDs, run `neo-ui groups`. Groups are defined for the whole instance in the Server space and are not the same as project roles. For more information, see the [Managing User Groups](../../ui/user-groups.md#ui-user-groups) page.

The field is a convenience for tailoring the interface, not a security boundary. Data access is enforced by the Squirro platform. For the full behavior, including what a restricted viewer sees when opening the URL directly, see the [Building Dashboard Components](building-dashboards.md#neo-extensions-building-dashboards) page.

## Sidebar Order and Visibility

By default, the sidebar shows the core items of the host application, which are `chat` and `search`, followed by the dashboards of the bundle in the order they are declared in the `dashboards` array. Reorder that array to change the relative order of your own dashboards.

The optional `nav` field of the manifest changes the arrangement for everyone who uses the bundle, including the core items:

```typescript
nav: {
  order: ['analytics', 'chat', 'search'],
  hidden: ['search'],
},
```

Core items are referenced by the reserved IDs `chat` and `search`. A dashboard of your bundle is referenced by its `route`.

> **Note**
>
> `topics` is no longer a core ID. The Topics dashboard was removed from the host application. A manifest that still names `topics` in `nav.order` or `nav.hidden` keeps building and running: the ID matches no item, so it is skipped with a warning in the browser console and the order of the surrounding entries is unaffected. A translation override keyed `topics:...` or `sidebar:nav.topics` is dropped silently, because overrides are forwarded to i18next without validation. Remove those entries when you next edit the manifest.

### The order Field

The listed IDs are appended after the items that are not listed, and appear in the order given. Items you leave out keep the default order and stay at the front of the sidebar. To place a dashboard first, list every ID, as in the example above.

- An ID that matches no core item and no dashboard route is skipped.
- A repeated ID keeps its first position.
- An ID that also appears in `hidden` stays hidden.

### The hidden Field

The listed IDs are removed from the sidebar for every viewer.

- A hidden dashboard also loses its route. A viewer who opens its URL is redirected to the first dashboard still available.
- A hidden core item loses its sidebar entry only. Its route stays reachable by direct URL.

The field composes with `allowedGroups`. A dashboard appears only when it passes both, so a dashboard that is hidden in `nav` is hidden even for the groups listed in `allowedGroups`.

> **Warning**
>
> The `nav` field tailors the interface. It is not a security boundary. Removing an entry from the sidebar does not restrict the data behind it, and the underlying APIs stay reachable. Never use it to gate sensitive content.

> **Note**
>
> Entries that are empty or are not text are dropped when the manifest is loaded, and an ID that matches no core item and no dashboard route is reported as a warning in the browser console. Avoid giving a dashboard the route `chat` or `search`. That route collides with the reserved ID of a core item, and any `nav` entry then applies to both.

## Translations

The `translations` field in the manifest lets your bundle provide localized strings and override host UI text. Run `neo-ui translations init` to set it up. The CLI handles everything automatically.

For more information, see the [Translations](translations.md#neo-extensions-translations) page.

## CLI Management vs Hand-Editing

The CLI manages the manifest automatically. In most workflows, you never need to edit this file by hand:

- `neo-ui create dashboard` adds a dashboard entry and creates the component file.
- `neo-ui remove dashboard` removes a dashboard entry and deletes the component file.
- `neo-ui translations init` adds the translations import and field.

For the rare cases where you do need to edit manually, such as reordering dashboards or changing an icon or route, the CLI and hand-edits coexist without conflict as long as the basic structure remains intact: preserve the `export default defineBundleManifest({ ... });` wrapper, and keep a `dashboards` array with objects containing `id`, `title`, `icon`, `route`, and `component`. Do not convert the manifest to a plain object literal, because the translation commands locate the closing `});`.

> **Tip**
>
> After hand-editing the manifest, verify it compiles by running `npx tsc --noEmit`.
