Building Providers
Building the UI
The custom-element contract, kedgeContext, navigation, and asset serving.
A provider’s frontend is a Web Component: a Vite-built main.js that registers a custom element as a side effect. The portal loads it once and mounts the element inline — no iframe, no postMessage, no Shadow DOM. The element runs in light DOM, so the portal’s :root CSS variables cascade in and your provider looks native by default.
Any framework works behind the element — quickstart is vanilla TS, most shipped providers wrap a Vue app, React or Lit fit the same seam. The contract with the portal is always: one entry script, one custom element.
The contract
When a user navigates to /providers/<name>, the portal:
- Injects a one-shot
<script src="/ui/providers/<name>/main.js">(cache-busted with?v=). - Waits for
customElements.whenDefined('kedge-provider-<name>'). - Appends the element and sets
element.kedgeContextas a JS property — the setter is your render trigger. - Re-sets
kedgeContexton theme toggle, token refresh, and workspace switch — handle partial updates. - Listens for
kedge-navigateCustomEvents bubbling up to drive the portal’s router/history.
The context shape:
export interface KedgeContext {
token?: string | null // caller's bearer token
user?: { email?: string; sub?: string } | null
tenant?: string | null // active workspace (logical cluster)
theme?: 'light' | 'dark' | 'system'
basePath?: string // e.g. /ui/providers/quickstart
subPath?: string // active sub-route / nav child
}
A minimal element
Straight from the quickstart provider (portal/src/main.ts + element.ts):
// main.ts — the entry script; side effects register everything
import { QuickstartElement } from './element'
import styles from './style.css?raw'
const TAG = 'kedge-provider-quickstart'
if (!customElements.get(TAG)) { // re-execution must be a no-op
const s = document.createElement('style')
s.textContent = styles // light DOM: namespace all rules under TAG
document.head.appendChild(s)
customElements.define(TAG, QuickstartElement)
}
// element.ts — property setter drives rendering
export class QuickstartElement extends HTMLElement {
private _ctx: KedgeContext | null = null
set kedgeContext(v: KedgeContext | null) {
this._ctx = v
this._render()
}
get kedgeContext() { return this._ctx }
connectedCallback() { this._render() }
// ...
}
Calling your backend
Derive the backend URL from basePath — never hardcode it:
const base = (ctx.basePath || '').replace(/^\/ui\/providers\//, '/services/providers/')
fetch(base + '/api/hello', {
headers: { Authorization: `Bearer ${ctx.token}` },
})
The hub’s backend proxy authenticates the request and injects the identity headers your server trusts — see Connectivity & proxies.
The Vite build
The portal hardcodes /ui/providers/<name>/main.js, so the build must emit exactly that:
// vite.config.ts
export default defineConfig({
base: '/ui/providers/quickstart/',
build: {
outDir: 'dist',
lib: {
entry: 'src/main.ts',
formats: ['iife'], // side effects run without a module loader
name: 'KedgeProviderQuickstart',
fileName: () => 'main.js', // no hash — the portal URL is stable
},
rollupOptions: {
output: {
chunkFileNames: 'assets/[name]-[hash].js', // lazy chunks under /assets/
assetFileNames: 'assets/[name]-[hash][extname]',
},
},
},
})
Why these choices: IIFE registers the element the moment the script runs; the un-hashed main.js keeps the portal’s URL stable across rebuilds; hashed chunks under assets/ route correctly because the hub’s UI proxy treats any path with a . in its last segment as an asset.
Navigation and sub-pages
Declare sub-nav in your CatalogEntry (ui.children: [{displayName: "Repositories", builtinRoute: "repositories"}]); the portal renders the items in the sidebar and passes the active child to your element via kedgeContext.subPath. Internal navigation flows the other way: dispatch a bubbling kedge-navigate CustomEvent with {detail: {path}} and the portal updates the browser URL. Keep your internal router (if any) on memory history — the portal owns the address bar.
Styling
- Light DOM: your CSS shares the page. Namespace every selector under your element tag (
kedge-provider-quickstart .panel {...}). - Use the portal’s CSS variables for colors and spacing so theme switches (light/dark) just work; re-render on
kedgeContext.themechanges for anything computed.
Serving the assets
Your Go binary embeds and serves the built assets — the standard pattern:
//go:embed all:portal/dist
var portalDist embed.FS
Serve /main.js, /assets/*, /icon.svg, and / (a standalone index.html fallback for direct visits) from your HTTP server; the hub’s UI proxy at /ui/providers/<name>/* forwards asset requests to you. Remember the build order: the frontend must be built before go build embeds portal/dist (the repo’s make targets chain npm run build first).