What is Swiss AI Hub?
Swiss AI Hub is an open-source, self-hosted AI platform for enterprises. One docker compose up starts ~30 integrated containers: an LLM gateway (LiteLLM), vector search (Milvus), data pipelines (Dagster), document parsing (MinerU), SSO (Keycloak), observability (Langfuse + OpenTelemetry), a chat UI (Open-WebUI), and more. You build custom agents, pipelines, and processes using the Python SDK; the platform provides the runtime.
What is this package?
This package is the admin and management UI -- one component of the larger platform. It is the interface where administrators configure agents, manage knowledge bases, monitor processes, inspect threads, assign roles, track costs, and build dashboards. It is not the chat UI (that's Open-WebUI) and not the backend API.
The admin UI is built with Nuxt 3, Vue 3, PrimeVue, and Tailwind CSS. It is published as a Nuxt layer -- a mechanism that lets you inherit an entire Nuxt application (pages, components, composables, plugins, config) and extend or override any part of it in your own project.
Should you use this package?
Probably not. Most deployments should use the pre-built Docker image, which ships the admin UI ready to go:
# docker-compose.yml
services:
admin-ui:
image: ghcr.io/bbvch-ai/aihub-core/web:latest
ports:
- "3333:80"The Docker image works out of the box with zero frontend code. Configuration (OIDC provider, API endpoint, WebSocket URL) is handled through environment variables at runtime.
Use this npm package only if you need to extend the UI with your own code -- adding custom pages, overriding components, modifying translations, or changing the theme. This is an SDK for building a custom frontend on top of Swiss Swiss AI Hub, not a standalone app.
When this package makes sense
| Use case | Example |
|---|---|
| Custom pages | Add an organization-specific dashboard, a domain-specific tool, or internal admin views that don't belong in the open-source project |
| Branding | Override the PrimeVue theme, replace the logo, adjust colors to match corporate identity |
| Translation overrides | Fix or extend translations, add a fifth language, change terminology to match your domain |
| Component overrides | Replace a built-in component with your own implementation |
| Custom plugins | Add organization-specific Nuxt plugins (analytics, feature flags, custom error tracking) |
| Custom auth flow | Extend the OIDC middleware for provider-specific requirements |
How Nuxt layers work
If you're not familiar with Nuxt layers, here's the idea: a layer is a full Nuxt application that another Nuxt project can inherit from using extends in nuxt.config.ts. When you extend this layer, you get all of its pages, components, composables, plugins, middleware, layouts, and configuration -- merged into your project automatically. Nuxt resolves conflicts by giving your project priority: if you define a component with the same name and path as one in the layer, yours wins. Same for pages, composables, and config keys.
This means you don't fork the repo or copy files. You install the package, extend it, and only write the code for what you want to change or add.
For a deeper understanding, see the official Nuxt layers documentation and the layer authoring guide.
Installation
npm install @swiss-ai-hub/web
# or
pnpm add @swiss-ai-hub/webInstall the required peer dependencies:
npm install primevue@4.5.5 vue@3.5.17Required dependency overrides
Do not skip this step. Without it your project ends up with two copies of Vue in node_modules, and the UI will either fail to build or render with broken behavior.
Nuxt and several of its modules depend on their own (newer) copy of Vue, while this layer is built and tested against an exact Vue version. With nothing forcing them to agree, your install resolves two different Vue versions -- and therefore two copies of @vue/runtime-core, the package that owns Vue's runtime identity. Two Vue runtimes means two separate reactivity systems: provide/inject stops working across the boundary, component instance checks fail, and you get cryptic errors like "Vue instance ... was created in a different application". The same single-instance requirement applies to PrimeVue, whose theme system relies on a global singleton (two instances → unstyled components).
The fix is to force the whole dependency tree onto one Vue version using your package manager's override mechanism. Add the vue override to your project's package.json — this is the one that actually breaks if omitted. Note the key differs per package manager (npm/pnpm use overrides, Yarn uses resolutions):
// npm
{ "overrides": { "vue": "3.5.17" } }// Yarn
{ "resolutions": { "vue": "3.5.17" } }// pnpm
{ "pnpm": { "overrides": { "vue": "3.5.17" } } }Then reinstall from a clean state so the lockfile is regenerated, and confirm a single Vue instance:
rm -rf node_modules package-lock.json # or yarn.lock / pnpm-lock.yaml
npm install
npm ls @vue/runtime-core # must print exactly one version: 3.5.17The same single-instance requirement applies to PrimeVue (its theme system is a global singleton); pinning the primevue peer to one version is enough — it does not need an override.
Optional — silence a
vue-routerpeer warning.@vueuse/routerdeclares avue-router@^4peer, but some transitive dependencies may pull invue-router5.x, which can surface a peer-range warning on install. It is harmless (the layer does not depend on that resolution), but if you want it gone, pinvue-routerto4.6.4scoped to@vueuse/routerso it never affects the router Nuxt itself uses:jsonc// npm: "overrides": { "@vueuse/router": { "vue-router": "4.6.4" } } // Yarn: "resolutions": { "@vueuse/router/vue-router": "4.6.4" } // pnpm: "pnpm": { "overrides": { "@vueuse/router>vue-router": "4.6.4" } }
Quick start
1. Create a Nuxt project
npx nuxi init my-aihub-frontend
cd my-aihub-frontend
npm installThen install this package, its peer dependencies, and -- importantly -- add the required dependency overrides. Skipping the overrides leaves two copies of Vue in your tree and the app will not work.
2. Extend the layer
Replace the generated nuxt.config.ts with:
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@swiss-ai-hub/web'],
// These dev defaults match infra/docker-compose.dev.yml + `make run-api`.
// For production, leave these declared (the keys must exist) but blank, and
// inject values at runtime via /config.js -- see Runtime configuration below.
runtimeConfig: {
public: {
env: 'dev',
oidc: {
clientId: 'aihub-frontend',
authorityUrl: 'http://localhost:8180/realms/aihub',
},
webui: {
url: 'http://localhost:8080',
},
ws: {
endpoint: 'ws://localhost:8000/api/v1/active/events/ws',
},
},
},
// Proxy API requests to the backend during development.
// In production, your reverse proxy (Traefik) handles this.
nitro: {
devProxy: {
'/api/v1': {
target: 'http://localhost:8000/api/v1',
changeOrigin: true,
ws: true,
},
},
},
})3. Start the platform
Make sure the Swiss AI Hub backend is running (either via docker compose up or locally with make run-api).
4. Run
npx nuxi devOpen http://localhost:3000. You get the full admin UI -- agents, processes, threads, knowledge bases, models, roles, dashboards, and chat -- running locally and pointing at your platform instance. Log in with your Keycloak credentials (default: admin / admin).
What the layer provides
Everything the admin UI needs ships inside this package:
| Category | What you get |
|---|---|
| Pages | Full file-based routing under /service/ -- agents, processes, threads, knowledge, models, roles, dashboards, chat, costs, evaluations |
| Components | ~170 Vue components organized by domain (Agent, Chat, Dashboard, Event, Navigation, Process, Thread, Workflow, ...) |
| Composables | Pinia-Colada query/mutation wrappers for every API resource |
| SDK client | Auto-generated TypeScript API client (HeyAPI) |
| Layouts | default (authenticated) and anonymous layouts |
| Middleware | OIDC auth guard on all routes |
| Plugins | OIDC client, config loader, ApexCharts |
| i18n | German, English, French, Italian (lazy-loaded YAML files) |
| Theme | PrimeVue Aura-based theme with dark mode support |
| FormKit config | Custom form inputs (agent selector, model select, knowledge database selector, icon selector, locale input, vector store input) |
Extending the UI
Add a custom page
Create a Vue file in pages/ and Nuxt merges it with the layer's routes. The layer's components and composables are auto-imported and available in your pages without any explicit imports:
<!-- pages/service/my-tool.vue -->
<template>
<StructuralScreen>
<StructuralColumn title="My Custom Tool">
<p>This page lives only in your deployment, not in the open-source project.</p>
<p>All layer components and composables are available here.</p>
</StructuralColumn>
</StructuralScreen>
</template>StructuralScreen is the full-height scrollable container used by every page. StructuralColumn provides a titled panel with built-in loading states -- pass :loading="true" to show a progress bar and suppress content until data is ready. These are the two layout primitives that all admin UI pages are built on.
To add navigation for your page, you can extend the sidebar by overriding the navigation component (see Override a component below).
Override translations
The layer ships i18n files for German, English, French, and Italian in i18n/locales/. To override specific keys or add a new language, create matching locale files in your project:
my-project/
i18n/
locales/
en.yaml # keys here override the layer's en.yaml
de.yaml
pt.yaml # add a new language# i18n/locales/en.yaml -- only the keys you want to change
agent:
title: "AI Assistants" # override "Agents" with your preferred term
my_tool:
title: "My Custom Tool" # add keys for your custom pagesYou also need to register new languages in your nuxt.config.ts:
export default defineNuxtConfig({
extends: ['@swiss-ai-hub/web'],
i18n: {
locales: [
{ code: 'pt', file: 'pt.yaml', name: 'Portugues' },
],
},
})Override the theme
The admin UI uses PrimeVue's styled mode with a customized Aura preset. Theming works through design tokens -- semantic color values that PrimeVue components reference. You override tokens to change the look of every component at once.
To create your own theme, start from the Aura preset and customize the tokens you want to change:
// themes/my-theme.ts
import { definePreset } from '@primeuix/themes'
import Aura from '@primeuix/themes/aura'
const MyPreset = definePreset(Aura, {
semantic: {
// Change the primary color palette (used for buttons, selections, highlights)
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
// Override light/dark mode colors
colorScheme: {
light: {
primary: {
color: '#2563eb',
inverseColor: '#ffffff',
hoverColor: '#1d4ed8',
activeColor: '#1e40af',
},
},
dark: {
primary: {
color: '#60a5fa',
inverseColor: '#172554',
hoverColor: '#93c5fd',
activeColor: '#bfdbfe',
},
},
},
},
})
export default {
preset: MyPreset,
options: {
darkModeSelector: '.dark', // must stay '.dark' to match the Tailwind dark mode config
},
}Then point your nuxt.config.ts at it:
// nuxt.config.ts
import { fileURLToPath } from 'url'
export default defineNuxtConfig({
extends: ['@swiss-ai-hub/web'],
primevue: {
importTheme: {
from: fileURLToPath(new URL('./themes/my-theme.ts', import.meta.url)),
},
},
})For the full list of design tokens you can customize, see the PrimeVue theming documentation and the Aura preset reference.
Override a component
Nuxt's layer system resolves components by name and directory path. If you place a component with the same name in the same directory structure, your version takes priority over the layer's:
my-project/
components/
Navigation/
Logo.vue # replaces the layer's Navigation/Logo.vue
Agent/
Card.vue # replaces the layer's Agent/Card.vueThis works for any component in the layer. You can inspect the layer's component directory structure in the source repository to find the exact names and paths.
Building for production
npx nuxi generateThis produces a fully static SPA in .output/public/ (the layer is client-only -- ssr: false). Because the output is static, there is no Node server at runtime to read environment variables. Instead, the layer ships a small runtime-config mechanism (see Runtime configuration) so you build one image and configure it per environment at container start -- including which backend API it talks to.
Dockerfile example
This mirrors how Swiss AI Hub ships the admin UI: build the static site, serve it with nginx, and generate /config.js from a template at startup so the same image works in any environment.
# 1. Build the static SPA. ENV must be unset (or anything other than 'dev') so
# the layer emits the <script src="/config.js"> tag that loads runtime config.
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npx nuxi generate
# 2. Serve with nginx; render /config.js from env vars on container start.
FROM nginx:alpine
RUN apk add --no-cache gettext # provides envsubst
COPY --from=build /app/.output/public /usr/share/nginx/html
COPY config.template.js /usr/share/nginx/html/config.template.js
CMD ["/bin/sh", "-c", "envsubst < /usr/share/nginx/html/config.template.js > /usr/share/nginx/html/config.js && nginx -g 'daemon off;'"]nginx must serve /config.js uncached and never fall back to index.html for it -- declare it before the SPA catch-all:
location = /config.js {
add_header Cache-Control "no-store" always;
default_type application/javascript;
try_files $uri =404;
}
location / { try_files $uri /index.html; }Runtime configuration
The layer reads all runtime configuration from runtimeConfig.public, populated in two phases:
- Build-time defaults -- whatever you put in
runtimeConfig.publicin yournuxt.config.ts. Used directly in dev, baked into the static build. - Runtime overrides (production) -- a
window.__AIHUB_CONFIG__object loaded synchronously from/config.jsbefore the app boots. A plugin shipped in this layer (plugins/0.runtime-config.client.ts) maps it intoruntimeConfig.public. This is how a single static build is configured per environment without rebuilding.
/config.js is rendered by envsubst from your config.template.js at container start (see the Dockerfile above). The layer injects the <script src="/config.js"> tag automatically for any non-dev build, and the mapping plugin runs in every app that extends the layer -- so you only supply the template:
// config.template.js -- envsubst replaces ${...} at container start.
// Include only the keys your deployment needs; unset ones resolve to defaults.
window.__AIHUB_CONFIG__ = {
API_BASE_URL: '${API_BASE_URL}',
OAUTH_CLIENT_ID: '${OAUTH_CLIENT_ID}',
OAUTH_AUTHORITY_URL: '${OAUTH_AUTHORITY_URL}',
WEBUI_URL: '${WEBUI_URL}',
WS_ENDPOINT: '${WS_ENDPOINT}',
}runtimeConfig.public key | window.__AIHUB_CONFIG__ key | Default | Description |
|---|---|---|---|
apiBaseUrl | API_BASE_URL | /api/v1 (same origin) | Backend API base URL -- see below |
oidc.clientId | OAUTH_CLIENT_ID | -- | OIDC client ID (Keycloak) |
oidc.authorityUrl | OAUTH_AUTHORITY_URL | -- | OIDC authority / realm URL |
webui.url | WEBUI_URL | -- | Open-WebUI URL (chat link) |
ws.endpoint | WS_ENDPOINT | -- | WebSocket endpoint for real-time agent events |
env | -- | -- | Set to dev locally (uses build-time values, skips /config.js) |
Declare the groups you want populated. The mapping plugin only writes into config groups your app already declared in
runtimeConfig.public(e.g.oidc: {},webui: {},ws: {}); in production set their build-time values to empty strings and let/config.jsfill them.apiBaseUrlis the exception -- it is always applied whenAPI_BASE_URLis present.
Pointing at your backend API
The admin UI talks to the Swiss AI Hub backend through a single base URL, runtimeConfig.public.apiBaseUrl. It defaults to the same origin as the UI (/api/v1) -- the simplest setup: put the UI and the API behind one reverse proxy (what the platform's Traefik does) and no API configuration is needed at all.
If your UI is served from a different origin than the API, set the base URL explicitly:
Dev / build-time -- set it in
nuxt.config.ts, or (recommended for local dev) keep/api/v1and proxy it with Nitro:tsexport default defineNuxtConfig({ extends: ['@swiss-ai-hub/web'], // Option A: point straight at the API origin runtimeConfig: { public: { apiBaseUrl: 'https://aihub.example.com/api/v1' } }, // Option B (same-origin dev): keep /api/v1 and proxy it nitro: { devProxy: { '/api/v1': { target: 'http://localhost:8000/api/v1', changeOrigin: true, ws: true } } }, })Production (static image) -- inject
API_BASE_URLat container start viaconfig.template.js. One image, any backend:bashdocker run -e API_BASE_URL=https://aihub.example.com/api/v1 my-aihub-frontend
Cross-origin caveats. A different API origin must allow your UI's origin via CORS, and your Keycloak client must list it as an allowed web/redirect origin. Same-origin (
/api/v1behind one proxy) avoids both. Do not callclient.setConfig({ baseURL: ... })in your ownapp.vueunless you deliberately want to hard-override this mechanism.
Peer dependencies
| Package | Version | Why |
|---|---|---|
primevue | 4.5.5 | UI component library -- must be a single instance to avoid theme-singleton conflicts (see overrides) |
vue | 3.5.17 | Framework runtime -- must be a single instance; enforce with the required overrides |
These exact versions are what the layer is built and published against. They are declared as
peerDependencies, so your project must provide them, and the overrides above ensure every transitive dependency resolves to the same single copy.
Tech stack
| Category | Technologies |
|---|---|
| Framework | Nuxt 3, Vue 3 (Composition API), TypeScript |
| UI | PrimeVue, Tailwind CSS, FormKit |
| State | Pinia-Colada (query/mutation caching) |
| Auth | oidc-client-ts (OpenID Connect) |
| API | HeyAPI (auto-generated TypeScript SDK) |
| Visualization | VueFlow (workflow graphs), ApexCharts (dashboards), Sigma.js (knowledge graphs) |
| Utilities | VueUse, lodash-es, date-fns |
| i18n | @nuxtjs/i18n (4 languages, lazy-loaded YAML) |
License
Copyright (C) 2024-2026 bbv Software Services AG.
AGPL-3.0-or-later — see packages/web/LICENSE. For the full per-package matrix (root, AGPL, and proprietary packages), see LICENSES.md.
Part of Swiss AI Hub. Built in Switzerland by bbv Software Services.
