Cockpit Integration
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
{
"$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.
Navigation (Sidebar Items)
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:
"navigation": [
{
"id": "orders-nav",
"label": "Orders",
"icon": "shopping-cart",
"path": "/orders"
}
]
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for this nav item |
label | string | Display text in sidebar |
icon | string | Icon name (Lucide icon, e.g., shopping-cart, user, settings) |
path | string | Route this nav item links to |
badge | string | Optional badge text (e.g., "3", "NEW") |
order | number | Sort 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.
{
"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:
"views": [
{
"id": "orders-list",
"route": "/orders",
"label": "Orders",
"title": "Order Management",
"component": "OrdersList"
},
{
"id": "orders-detail",
"route": "/orders/:id",
"label": "Order Detail",
"component": "OrderDetail"
}
]
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
route | string | URL route (supports params like :id) |
label | string | Breadcrumb label |
title | string | Page title (shown in browser tab and header) |
component | string | Vue component name (must exist in your app) |
parent | string | Parent route (for breadcrumbs) |
Component Implementation:
Your app must export a Vue component matching the component name:
// 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:
"widgets": [
{
"id": "orders-summary",
"label": "Order Summary",
"description": "Today's order count and revenue",
"component": "OrderSummaryWidget",
"size": "medium"
}
]
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
label | string | Display name |
description | string | Tooltip or help text |
component | string | Vue component name |
size | string | small, 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.
"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"
}
]
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
label | string | Button text |
description | string | Tooltip |
target_view | string | Route where this button appears (e.g., /orders/:id) |
component | string | Vue component (button component) |
color | string | Button color (primary, secondary, danger, success) |
icon | string | Lucide icon name (optional) |
When a Customer views an order detail page, Cockpit injects the "Sync to ERP" button. Your component handles the click:
// 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— Commerceuser,users,user-circle— Peoplesettings,sliders— Configurationbell,mail— Notificationsbar-chart,line-chart,pie-chart— Analyticslink,external-link— Integrationcalendar,clock— Timecheck,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:
// 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
- Concept — Understand the declarative app model
- Lifecycle — App versioning and upgrades
- Billing & Events — Marketplace billing and event publishing