Cockpit Integration

Define UI extension points with cockpit.json

This is for app developers whose app needs to appear in Cockpit — with a sidebar entry, a full-page view, a dashboard widget, or an action button on another app's screen. If your app is purely a backend service (only exposes an API, no UI in Cockpit), you don't need this page.

Cockpit isn't a single application that has every feature hard-coded. It's a Vue.js shell that composes its UI at runtime from the apps a customer has installed. Each app declares its UI contributions in cockpit.json — what navigation items to add, what routes to register, what widgets to put on the dashboard. When the customer opens Cockpit, the shell reads each installed app's cockpit.json and weaves the contributions together.

The Vue components that implement your views and widgets are deployed alongside your app. The configuration in cockpit.json is what tells Cockpit where in the UI those components show up.

This page documents the four extension point types — navigation, views, widgets, action_buttons — and how to wire each one up.

Complete Example

JSON
{
  "$schema": "https://revenexx.com/schemas/cockpit.schema.json",
  "navigation": [
    {
      "id": "orders-nav",
      "label": "Orders",
      "icon": "shopping-cart",
      "path": "/orders"
    }
  ],
  "views": [
    {
      "id": "orders-list",
      "route": "/orders",
      "label": "Orders",
      "title": "Order Management",
      "component": "OrdersList"
    },
    {
      "id": "orders-detail",
      "route": "/orders/:id",
      "label": "Order Detail",
      "component": "OrderDetail"
    }
  ],
  "widgets": [
    {
      "id": "orders-summary",
      "label": "Order Summary",
      "description": "Today's order count and revenue",
      "component": "OrderSummaryWidget"
    }
  ],
  "action_buttons": [
    {
      "id": "sync-erp",
      "label": "Sync to ERP",
      "description": "Send this order to the ERP system",
      "target_view": "/orders/:id",
      "component": "SyncToERPButton"
    }
  ]
}

Extension Points

There are four kinds of contribution an app can make to Cockpit, and they differ by where they appear. Navigation items live in the sidebar, views own full pages, widgets sit on the dashboard, and action buttons are injected into pages that may belong to another app entirely. Most apps use the first two; widgets and action buttons are how you surface your app in places a customer is already looking.

A navigation entry is the top-level way into your app — the line in Cockpit's sidebar a user clicks to reach it. At its simplest it's a label, an icon, and the path it links to:

JSON
"navigation": [
  {
    "id": "orders-nav",
    "label": "Orders",
    "icon": "shopping-cart",
    "path": "/orders"
  }
]
FieldTypeDescription
idstringUnique identifier for this nav item
labelstringDisplay text in sidebar
iconstringIcon name (Lucide icon, e.g., shopping-cart, user, settings)
pathstringRoute this nav item links to
badgestringOptional badge text (e.g., "3", "NEW")
ordernumberSort order (lower numbers appear first)

The optional badge is worth knowing about: it puts a small count or label next to the item, which is how you'd surface something like an unread count without building any custom chrome.

JSON
{
  "id": "notifications",
  "label": "Messages",
  "icon": "bell",
  "path": "/messages",
  "badge": "5"
}

Views (Full-Page Routes)

A view is a full page inside Cockpit, mapped to a route. This is where most of your app's actual UI lives — list pages, detail pages, anything that fills the main content area. Routes can carry parameters (:id), so a single view definition serves every order in the example below:

JSON
"views": [
  {
    "id": "orders-list",
    "route": "/orders",
    "label": "Orders",
    "title": "Order Management",
    "component": "OrdersList"
  },
  {
    "id": "orders-detail",
    "route": "/orders/:id",
    "label": "Order Detail",
    "component": "OrderDetail"
  }
]
FieldTypeDescription
idstringUnique identifier
routestringURL route (supports params like :id)
labelstringBreadcrumb label
titlestringPage title (shown in browser tab and header)
componentstringVue component name (must exist in your app)
parentstringParent route (for breadcrumbs)

Component Implementation:

Your app must export a Vue component matching the component name:

JavaScript
// ordersApp.vue (or component library)
export { OrdersList, OrderDetail }

// OrdersList.vue
<template>
  <div>
    <h1>Orders</h1>
    <OrdersTable />
  </div>
</template>

<script setup>
// Component receives tenant context via props or API
const route = useRoute()
const tenantId = route.query.tenant_id
</script>

Widgets (Dashboard Cards)

A widget is a card a customer can place on their Cockpit dashboard — a compact, glanceable summary rather than a full page. Where a view is somewhere users navigate to, a widget brings a slice of your app to the home screen they already land on. The size hints at how much room it takes in the dashboard grid:

JSON
"widgets": [
  {
    "id": "orders-summary",
    "label": "Order Summary",
    "description": "Today's order count and revenue",
    "component": "OrderSummaryWidget",
    "size": "medium"
  }
]
FieldTypeDescription
idstringUnique identifier
labelstringDisplay name
descriptionstringTooltip or help text
componentstringVue component name
sizestringsmall, medium, large (grid layout)

Customers can add widgets to their dashboard. Your widget receives tenant context and can query the generated data APIs or your app APIs.

Action Buttons (Injected Buttons)

Action buttons are the most interesting of the four, because they let your app add a control to a page it doesn't own. You name a target_view — even one belonging to another app — and Cockpit injects your button there. This is how cross-app integration surfaces in the UI: a compliance app can drop a "Mark hazmat-validated" button onto the Orders detail page without the Orders app knowing it exists.

JSON
"action_buttons": [
  {
    "id": "sync-erp",
    "label": "Sync to ERP",
    "description": "Send this order to the ERP system",
    "target_view": "/orders/:id",
    "component": "SyncToERPButton",
    "color": "primary"
  }
]
FieldTypeDescription
idstringUnique identifier
labelstringButton text
descriptionstringTooltip
target_viewstringRoute where this button appears (e.g., /orders/:id)
componentstringVue component (button component)
colorstringButton color (primary, secondary, danger, success)
iconstringLucide icon name (optional)

When a Customer views an order detail page, Cockpit injects the "Sync to ERP" button. Your component handles the click:

JavaScript
// SyncToERPButton.vue
<template>
  <button @click="syncToERP" :disabled="syncing">
    {{ syncing ? 'Syncing...' : 'Sync to ERP' }}
  </button>
</template>

<script setup>
const props = defineProps({
  entityId: String,  // e.g., order ID from route param
  entityType: String // e.g., 'orders'
})

const syncing = ref(false)
const syncToERP = async () => {
  syncing.value = true
  await fetch('/api/sync-erp', {
    method: 'POST',
    body: JSON.stringify({ order_id: props.entityId })
  })
  syncing.value = false
}
</script>

Icon Names (Lucide)

Common icons:

  • shopping-cart, shopping-bag — Commerce
  • user, users, user-circle — People
  • settings, sliders — Configuration
  • bell, mail — Notifications
  • bar-chart, line-chart, pie-chart — Analytics
  • link, external-link — Integration
  • calendar, clock — Time
  • check, x, alert — Status

See Lucide Icons for the full list.

How Cockpit Renders

It's worth understanding the assembly step, because it explains why your contributions appear next to other apps' contributions. When Cockpit loads, it asks Console for the list of apps the tenant has installed, reads each one's cockpit.json, and merges all of their declarations into a single picture of the UI. From that merged picture it builds the sidebar from every app's navigation items, turns every views entry into a live route, makes every widgets entry available on the dashboard, and injects each action_buttons entry into whichever view it targets. Your app's pieces and a dozen other apps' pieces are composed into one coherent interface at runtime — which is exactly why you declare where things go rather than building the shell yourself.

Scope and Isolation

That composition model also sets the boundaries you work within. Cockpit owns routing, so your app never renders its own <router-view> — it just declares routes and hands over components. Those components receive what they need as props or via the route, the current tenant and any entity ID included, so there's no global state to reach into and each view or widget stays self-contained. Styling comes from the Studio Design System (shadcn-vue), which Cockpit applies for you, so your components inherit the platform's look without you shipping a stylesheet. The net effect is that your UI feels native because it's genuinely running inside the same shell as everything else.

Component Hints

When writing Vue components for Cockpit:

JavaScript
// OrdersList.vue — Receives no special props
<script setup>
import { useRoute } from 'vue-router'
const route = useRoute()
const tenantId = route.query.tenant_id // Available from JWT/Cockpit context
</script>

// OrderSummaryWidget.vue — Lightweight dashboard widget
<script setup>
import { ref, onMounted } from 'vue'
const stats = ref(null)
onMounted(async () => {
  const res = await fetch('/api/orders/stats')
  stats.value = await res.json()
})
</script>

// SyncToERPButton.vue — Receives entity context via props
<script setup>
defineProps({
  entityId: String,
  entityType: String
})
</script>

Best Practices

Because your UI shares the shell with every other installed app, the guiding principle is to be a good citizen of a space you don't own.

Keep the sidebar uncluttered. Navigation items should be concise (two or three words) and few — add a top-level entry for the way into your app, not for every page within it, and group related views under a single nav item. A tidy sidebar is a shared resource; every extra entry you add is one the customer has to scan past to reach someone else's.

Look and behave like the platform. Stick to standard Lucide icons rather than custom artwork so your app reads as part of Cockpit, give action buttons help text via description so their purpose is clear, and always handle loading and error states — a half-rendered widget undermines the whole dashboard, not just your card.

Keep widgets light and context honest. A dashboard widget is glanced at, not worked in, so keep it responsive and push any heavy computation to an API call rather than the render path. And never hard-code tenant context; take it from the route or props every time, so the same component serves every tenant correctly.

Next Steps

Was this page helpful?