Introduction
Kanidm Dashboard is a modern web console for Kanidm — the identity provider. It is a SolidJS single-page application that provides an admin interface and user self-service portal over the Kanidm REST API.
Who is this for?
- Organisations running Kanidm who want an admin UI and user portal without building their own frontend.
- Operators who prefer static deploy-time config (theme, branding, login message) over a writeable app database.
- Developers integrating Kanidm OAuth2 who need a reference client implementation.
Design principles
- API-spine first. The dashboard talks to Kanidm through a same-origin
proxy — it is a static asset bundle served alongside Kanidm’s
/v1,/oauth2, and/uiroutes by a reverse proxy such as Caddy. - Adapter seam. All data access goes through a
DashboardDataSourceinterface with two adapters:KanidmDataSource(real API) andMockDataSource(in-memory demo). Switching between them is a single config field. - Static config. Deploy-time settings (theme, company name, logo, login message) live in a JSON file — no database required. Kanidm-native branding (domain display name, domain image, OAuth2 app icons) is read from and written to Kanidm directly.
Features
- Admin console — manage people, groups, and OAuth2 applications with review-before-submit wizards and access-impact previews.
- Self-service portal — users can manage credentials: password, TOTP, passkeys, SSH public keys, RADIUS, and Unix account settings.
- Credential update wizard — multi-step state machine for enrolling new credentials via reset tokens.
- Relationship explorer — visualise how people, groups, and applications connect.
- Native Kanidm branding — domain display name, domain image, and OAuth2 application icons.
- OAuth2 / OIDC consent flow — built-in consent page and native OAuth2 authorization endpoint.
- E2E test suite — Playwright tests against a real Kanidm server covering admin workflows, self-service, OAuth2 consent, credential resets, WebAuthn passkeys, and recovery email.
Getting Started
Prerequisites
- Node.js ≥ 22
- Vite+ (
vp) — the project usesvp(Vite+) as its toolchain entry point - just — command runner for project recipes
- openapi-nexus — Rust CLI for generating the TypeScript SDK
- Kanidm (optional) — for real-API development and E2E tests
Quick start
# Install dependencies
vp install
# Generate the Kanidm TypeScript SDK (required before vp dev or vp build)
just generate-sdk
# Start the dev server
vp dev
Open http://localhost:5173. The default config uses Kanidm mode — use
scripts/fixtures/dashboard.config.mock.json as public/dashboard.config.json
for pre-seeded demo data without a Kanidm backend.
Local Kanidm
Start Kanidm and generate TLS certificates:
./scripts/dev-kanidm-bootstrap.sh
This starts Kanidm on https://localhost:18443. During vp dev, Vite proxies
Kanidm routes (/v1, /oauth2, /.well-known, /docs) to that server.
Recover the idm_admin password:
docker compose -f deploy/local/docker-compose.yml exec kanidm \
kanidmd recover-account idm_admin -c /data/server.toml
Copy .env.local.example to .env.local with the recovered password.
Same-origin preview
Build the dashboard and serve it through Caddy for a production-like setup:
vp build
docker compose -f deploy/local/docker-compose.yml up -d dashboard-proxy
Open https://localhost:9443. Caddy serves dashboard assets and proxies Kanidm
routes.
Commands
| Command | Description |
|---|---|
just install | Install dependencies |
just generate-sdk | Regenerate the Kanidm TypeScript SDK |
just check | Type-check and lint |
just test | Run unit tests |
just build | Production build to dist/ |
just audit | Verify production build integrity |
just ci-fast | Run all fast quality checks |
just book | Build the documentation site |
just book-serve | Serve documentation with live reload |
Architecture
Request flow
browser
│
├─ / → dashboard SPA (SolidJS + Vite)
├─ /v1/* → Kanidm REST API (proxied by Caddy / Vite dev server)
├─ /oauth2/* → Kanidm OAuth2 endpoints
└─ /dashboard.config.json → static deploy-time config
Source layout
src/
domain.ts Shared types: Person, Group, Application, ConsoleState
data-source.ts DashboardDataSource interface + KanidmDataSource + MockDataSource
store.tsx SolidJS context: state, auth, credential operations
kanidm-auth.ts Auth state machine: password, TOTP, passkey, backup code, security key
kanidm-mappers.ts Kanidm API response → domain model mapping
kanidm-composite.ts Multi-step create operations: groups, OAuth2 apps
kanidm-error.ts HTTP error types and auth-failure detection
seed.ts Initial state and demo fixtures
App.tsx Router, layout shells, and page components
components/ Shared UI components: ErrorBox, OptionGrid, GlassPanel, etc.
generated/ TypeScript SDK generated from Kanidm OpenAPI spec
Data source seam
The DashboardDataSource interface defines 31 CRUD methods. Two adapters
implement it:
KanidmDataSource— calls the Kanidm REST API via a generated TypeScript SDK. The SDK is generated fromspec/kanidm-openapi.jsonusing openapi-nexus.MockDataSource— in-memory state persisted tolocalStorage. Seeds fromsrc/seed.tswith demo people, groups, and applications.
Switching between them is a single config field (dataSource.mode) — no code
changes required.
Store
store.tsx provides a SolidJS context (ConsoleProvider) that wraps the data
source. It exposes:
- State — reactive
ConsoleStatewith people, groups, applications, and branding. - Auth — login flows (password, TOTP, passkey, backup code, security key).
- Mutations — CRUD operations that go through
mutateKanidm()(writes) orreadKanidm()(reads), both handling error recovery and state reload.
Auth flow
Kanidm uses a stepped authentication protocol. Each step returns an
X-KANIDM-AUTH-SESSION-ID header that must be sent with the next request.
The dashboard implements this state machine in kanidm-auth.ts:
init2— begin authentication with usernamechoose— the server responds with available mechanismsbegin— select a mechanism (password, passkey, etc.)cred— submit credentials
Credential update state machine
The credential update wizard (src/pages/reset-credentials.tsx) and
self-service update page (src/pages/enrol.tsx) use Kanidm’s multi-step
credential protocol:
_exchange_intent— exchange an admin-issued reset token for a session, or/v1/person/{id}/_credential/_update— begin an authenticated self-service session_status— query current credential state_update— stage credential changes (password, TOTP, passkeys, etc.)_commit— commit all staged changes atomically
Configuration
The dashboard reads a single JSON file at /dashboard.config.json:
{
"dataSource": {
"mode": "kanidm",
"apiBasePath": "",
"openApiPath": "/docs/v1/openapi.json"
},
"siteName": "My Org",
"logoUrl": "https://example.com/logo.svg",
"loginMessage": "Sign in with your organisation account",
"adminGroup": "idm_admins",
"theme": {
"mode": "system",
"preset": "blue"
}
}
Fields
| Field | Type | Description |
|---|---|---|
dataSource.mode | "kanidm" | "mock" | "kanidm" for real API, "mock" for demo data |
dataSource.apiBasePath | string | Base path for the Kanidm API (empty for same-origin) |
dataSource.openApiPath | string | Path to the Kanidm OpenAPI spec |
siteName | string | Site title shown in the browser tab and login page |
logoUrl | string | Fallback logo when native Kanidm domain branding is unavailable |
loginMessage | string | Message displayed on the login page |
adminGroup | string | Kanidm group whose members get admin console access |
theme.mode | "light" | "dark" | "system" | Colour scheme mode |
theme.preset | string | Colour preset name |
Static vs. native branding
The dashboard has two branding layers:
- Static config (
dashboard.config.json) — deploy-time settings: company name, fallback logo, login message, and theme. Update the file and restart. - Native Kanidm — domain display name, domain image, and OAuth2 application icons are read from (and written to) Kanidm directly via the REST API. These take precedence over static config when available.
Demo mode
For development and demos without a Kanidm backend, use
scripts/fixtures/dashboard.config.mock.json:
cp scripts/fixtures/dashboard.config.mock.json public/dashboard.config.json
vp dev
The mock data source pre-seeds people, groups, and applications so every page is explorable immediately.
Development
Setup
vp install
just generate-sdk
vp dev
See the Getting Started guide for detailed prerequisites and local Kanidm setup.
Project scripts
just check # Type-check and lint
just test # Run unit tests
just build # Production build
just audit # Verify production build integrity
just ci-fast # Run all fast quality checks
Code conventions
- TypeScript strict mode — all code is fully typed;
anyis avoided. - SolidJS reactivity — use signals,
createEffect, andsetState; avoid direct DOM manipulation. - Component structure — shared UI components live in
src/components/; page components are defined insrc/App.tsx. - Data access — always go through the
DashboardDataSourceinterface; never call the SDK directly from components.
Adding a new page
- Define the route in
App.tsxin theSwitchPublicorSwitchPrivatecomponent. - Create the page component in
App.tsx(or a new file insrc/pages/). - Use
useConsole()to access state and mutations. - Follow the existing patterns for error handling (
ErrorBox), busy states, and review-before-submit flows.
SDK Generation
The Kanidm TypeScript SDK is generated from the checked-in OpenAPI spec at
spec/kanidm-openapi.json using
openapi-nexus.
Prerequisites
Install openapi-nexus:
cargo install --path ../openapi-nexus
Generate
just generate-sdk
This runs openapi-nexus generate with:
- Input:
spec/kanidm-openapi.json - Output:
src/generated/kanidm-sdk/ - Generator:
typescript-fetch - Naming:
camelCaseproperty naming,PascalCasefile naming - Target: ES2022, ESNext modules
The generated SDK includes:
- API classes in
apis/— one per Kanidm tag (PersonApi, GroupApi, Oauth2Api, etc.) - Model types in
models/— TypeScript interfaces withfromJSON/toJSONconverters - Runtime in
runtime/—Configuration, fetch wrapper, response types
Version control
The src/generated/ directory is excluded from version control via
.gitignore. Run just generate-sdk after cloning the repository and after
updating spec/kanidm-openapi.json.
Testing
Unit tests
just test
Tests cover:
- Data source seam —
MockDataSourceandKanidmDataSourcebehaviour - Auth flows — login with password, password+TOTP, backup code
- Composite operations — group and OAuth2 application creation
- Mappers — Kanidm API response parsing and domain model mapping
E2E tests
# Full suite against a real Kanidm
just e2e-kanidm
# WebAuthn / passkey test
just e2e-webauthn
# Recovery email test
just e2e-recovery-mail
The main E2E suite (scripts/e2e-real-kanidm.mjs) uses Playwright with Chromium
against a real Kanidm instance. It verifies 22 behaviours:
- Expired session redirect
- Admin login
- Group creation (parent and child)
- Person creation with group membership
- Group membership toggling
- Nested relationship resolution
- OAuth2 application creation
- Application image upload and reset
- Domain image upload and reset
- Credential setup (password, TOTP, backup codes) via reset token
- Native OAuth2 discovery, consent, and access denial
- Non-admin portal login with backup code
- Non-admin route guards (admin pages redirect to portal)
- Non-admin mutation denial (direct API calls are rejected)
- Profile read-only enforcement
- RADIUS self-service
- SSH public key management
- Reauth flow
- Session revocation
- Unix credential self-service
- Logout
- Fixture cleanup
Each run creates unique test fixtures and cleans them up on completion, even when tests fail.
Requirements
KANIDM_PASSWORDin.env.local- Running Kanidm instance (via
./scripts/dev-kanidm-bootstrap.sh) - Caddy proxy (via
docker compose -f deploy/local/docker-compose.yml up -d dashboard-proxy)
Production artifact audit
just audit
Verifies the production build output: checks that all expected files exist, the JavaScript bundle is non-empty, SPA fallback works, and the config file is valid.
Deployment
The dashboard is distributed as a container image that bundles the static SPA with an embedded Caddy reverse proxy. Caddy serves the dashboard and proxies Kanidm API routes internally — you only need to expose one port.
Quick start
docker run -d -p 8080:8080 \
-e KANIDM_UPSTREAM=https://your-kanidm-server:8443 \
ghcr.io/adamcavendish/kanidm-dashboard:0.0.2
Open http://localhost:8080.
If your Kanidm server runs in the same Docker network, use the container name as the upstream:
# docker-compose.yml
services:
kanidm:
# ... your existing Kanidm setup ...
dashboard:
image: ghcr.io/adamcavendish/kanidm-dashboard:0.0.2
environment:
KANIDM_UPSTREAM: https://kanidm:8443
ports:
- "8080:8080"
A full reference compose file is available at
deploy/container/docker-compose.yml.
TLS between dashboard and Kanidm
The dashboard communicates with Kanidm over HTTPS. The container’s Caddy server uses the system trust store — it works out of the box when Kanidm has a publicly trusted certificate (e.g. Let’s Encrypt).
Self-signed certificates
If Kanidm uses a self-signed certificate (the default when setting up
kanidmd), you need two things:
- Mount the Kanidm CA certificate into the container
- Mount a custom Caddyfile that trusts it
Where to find chain.pem: Kanidm generates this during initial setup.
It is typically at /data/certs/chain.pem inside the Kanidm container, or
in the directory you mounted to kanidm’s /data/certs.
Custom Caddyfile (dashboard-caddyfile):
{
auto_https off
}
:8080 {
encode zstd gzip
@dashboardConfig path /dashboard.config.json
handle @dashboardConfig {
header Cache-Control "no-store"
root * /config
file_server
}
@kanidm path /ui* /v1* /oauth2* /pkg* /hpkg* /.well-known* /docs* /status
handle @kanidm {
reverse_proxy {$KANIDM_UPSTREAM:https://kanidm:8443} {
transport http {
tls_server_name kanidm.example.com
tls_trusted_ca_certs /certs/chain.pem
}
}
}
handle {
root * /srv/dashboard
try_files {path} /index.html
file_server
}
}
Replace kanidm.example.com with your Kanidm server’s domain name — the
one in the certificate’s Subject Alternative Name.
Docker Compose:
dashboard:
image: ghcr.io/adamcavendish/kanidm-dashboard:0.0.2
environment:
KANIDM_UPSTREAM: https://kanidm:8443
volumes:
- ./dashboard-caddyfile:/etc/caddy/Caddyfile:ro
- ./certs/chain.pem:/certs/chain.pem:ro
ports:
- "8080:8080"
| Directive | Purpose |
|---|---|
tls_server_name | Overrides the TLS SNI to match the certificate’s domain. The Docker container hostname (kanidm) differs from the domain in the certificate. |
tls_trusted_ca_certs | Trusts the Kanidm CA certificate so the self-signed cert is accepted. |
tls_insecure_skip_verify | Avoid. Disables all TLS verification — accepts any certificate. Use only as a temporary workaround. |
Public certificates
No extra configuration is needed. The dashboard’s Caddy server trusts
public CAs by default. Set KANIDM_UPSTREAM to your Kanidm server’s
HTTPS URL and the TLS handshake will verify normally.
Reverse proxy (standalone)
If you prefer to run the dashboard without the container’s embedded Caddy,
serve dist/ with your own reverse proxy:
# Build from source
vp build
# Serve dist/ with your web server, proxying Kanidm routes
Example Caddy configuration:
kanidm.example.com {
handle /v1/* {
reverse_proxy https://kanidm-server:8443
}
handle /oauth2/* {
reverse_proxy https://kanidm-server:8443
}
handle {
root * /srv/dashboard
try_files {path} /index.html
file_server
}
}
Configuring the dashboard
Place a dashboard.config.json at the web root (next to index.html).
The container image includes a default config. See
Configuration for all options.
To use a custom config with the container:
volumes:
- ./my-dashboard.config.json:/config/dashboard.config.json:ro
CI/CD
Container images are published to ghcr.io/adamcavendish/kanidm-dashboard
on every semver tag ([0-9]*). See .github/workflows/container-image.yml.