Sanskrit: अधार (Adhāra) – Foundation
Adhar Console is a unified operator UI over the Adhar platform. It doesn't replace any of the open-source tools underneath — it aggregates them into one coherent, tenant-aware experience that follows the 6D lifecycle model:
| Phase | What happens here | Backed by |
|---|---|---|
| Define | Requirements, epics, agile issues, OKRs | Plane.so |
| Design | ADRs, design tokens, diagrams, visual builder | adhar-ui builder, Mermaid, Storybook |
| Develop | Source control, PRs, CI | Gitea, Argo Workflows |
| Deliver | GitOps, promotion, rollouts, registry, policy | Argo CD, Kargo, Argo Rollouts, Harbor, Kyverno |
| Discover | Observability (logs, metrics, traces) | Grafana, Loki, Mimir, Tempo, Prometheus, OpenTelemetry, Beyla |
| Decide | Cross-cutting analytics — DORA, health, spend | Derived from every phase |
| Platform | Cross-cutting Kubernetes dashboard | Kubernetes API + Adhar-stack CRDs (Crossplane, ArgoCD, Kargo, Kyverno, …) |
Plus a dedicated Workspace area for SaaS primitives: onboarding, org & project management, members & teams, environments, API tokens, audit log, plans, quotas, and webhooks.
Sign in. One confidential OIDC client — your password never touches the console.
Overview. Cluster health, delivery state, capacity and policy on one screen — every number read live from Kubernetes and Argo CD.
- Transparency. Every screen links back to its upstream open-source project. The Platform status page shows real versions and source URLs for Gitea, ArgoCD, Kargo, Keycloak, Kyverno, Harbor, Plane, and the full LGTM stack. No vendored black boxes.
- Open-core. Source-available under Apache 2.0. The managed offering covers operations, SLAs, and enterprise features — never capability.
- Composable. Each phase is a Module Federation remote. Teams can own a phase end-to-end, deploy independently, or vendor one out.
- Single origin, single process. The whole console — SPA, every federated remote, and the BFF API — is served by one small Deno server from one origin.
flowchart LR
B["Browser<br/><small>SPA host + 8 federated remotes</small>"]
subgraph origin["console.<domain> · one origin, one process"]
direction TB
S["apps/console/server.ts<br/><small>Deno.serve — serves the SPA and hosts the BFF</small>"]
AU["/api/auth/*"]
K8["/api/k8s/*"]
SV["/api/svc/<tool>/*"]
ST["/api/store/* · /api/prefs<br/>/api/notifications"]
AI["/api/ai/*"]
S --- AU & K8 & SV & ST & AI
end
KC["Keycloak<br/><small>confidential OIDC client</small>"]
API["kube-apiserver"]
TOOLS["Backing tools<br/><small>Argo CD · Gitea · Harbor · Kargo<br/>Grafana · Loki · Mimir · Tempo · …</small>"]
PG[("Postgres<br/><small>Drizzle</small>")]
B -->|"static SPA + remotes"| S
B -->|"fetch /api/* · HttpOnly session cookie"| S
AU -->|"code exchange + JWKS<br/>tokens never reach the browser"| KC
K8 -->|"as the signed-in user<br/>OIDC impersonation — their RBAC, their audit"| API
SV -->|"upstream credential injected server-side"| TOOLS
ST --> PG
AI -.->|"read + propose"| API
classDef edgeNode fill:none,stroke:#6366f1,stroke-width:1.5px
classDef apiNode fill:none,stroke:#94a3b8,stroke-width:1px
classDef extNode fill:none,stroke:#10b981,stroke-width:1.5px
class B,S edgeNode
class AU,K8,SV,ST,AI apiNode
class KC,API,TOOLS,PG extNode
- Client is a Vite SPA (React 19 + TanStack Router/Query) with each 6D
phase loaded as a
@module-federation/viteremote. - Server (
apps/console/server.ts) is a dependency-lightDeno.servethat statically serves the built SPA + remotes and hosts the BFF via framework-agnosticRequest → Responsehandlers. No SSR framework. - Auth is a confidential OIDC client: the server does the code exchange,
verifies the ID token against Keycloak's JWKS (
jose), refreshes transparently, and keeps a stateless signed session in an HttpOnly cookie — tokens never reach the browser. - Kubernetes is reached through
/api/k8s/*as the signed-in user (per-user OIDC impersonation): the user's Keycloak access token — minted with the apiserver'skubernetesaudience +groupsclaim — is forwarded to the kube-apiserver, so the apiserver enforces that user's RBAC + native audit. The console holds no cluster privilege of its own for user-facing calls. - Backing tools are reached through
/api/svc/<tool>which injects the upstream credential server-side. - State: live infra comes from the Kubernetes API / backing tools; Postgres
(Drizzle) holds the console's own state — preferences, notifications, and
the document store (
/api/store/*): OKRs, saved views, custom roles, webhooks, design docs, API specs — tenant-scoped, shared across a tenant's members. - No stubs in a running system. Everything above talks to real backends. The
in-memory
.stub()clients exist only for tests/offline (opt-in viamode:'stub');pnpm devconnects to a locally-running adhar cluster (below).
See ARCHITECTURE.md and docs/architecture/.
| Concern | Choice |
|---|---|
| Runtime | Deno 2 (deno run, npm:/jsr: imports) |
| Package mgr | pnpm workspaces (for Vite build-time resolution) |
| Client | Vite SPA · React 19.2 · TanStack Router + Query · zod |
| Microfrontend | @module-federation/vite — client-side load, one origin in prod |
| Server | Standalone Deno server (Deno.serve) — SPA host + BFF API |
| UI | @adhar-ui/* — React 19.2, Tailwind v4, CVA, Mitosis primitives |
| Auth | Keycloak OIDC (confidential client) → signed HttpOnly session cookie |
| Database | Postgres + Drizzle ORM (postgres.js) — prefs, notifications, doc store |
| Deploy | OCI image (ghcr.io/adhar-io/adhar-console) on the Adhar Kubernetes platform |
Read this first. The console is a window onto a cluster, not a standalone app. It has no demo mode and no stub fallback — every screen reads a real Kubernetes API and real backing tools. You need an Adhar platform to point it at before anything else works. That single fact is the most common reason a first run fails.
The console is one of the packages the Adhar platform ships, so the platform comes
first. If you don't have one, install it with
adhar up, then confirm it is reachable:
kubectl get nodes # a cluster you can reach
kubectl -n adhar-system get svc keycloak # …that is an Adhar platform✅ You should see a list of ready nodes, and a keycloak Service. If either
command fails, stop here — nothing below will work.
| I want to… | Go to | Needs |
|---|---|---|
| 🐳 See it running, least setup | Step 3A — run the container | Docker |
| 🛠️ Change the console's code | Step 3B — develop | Deno, pnpm, Node |
| 🚀 Install it for a team | Step 3C — deploy | platform admin |
For the develop path you additionally need:
| Tool | Version | Check |
|---|---|---|
| Deno | ≥ 2.0 | deno --version |
| pnpm | ≥ 10 | pnpm --version |
| Node | ≥ 20 | node --version |
adhar-ui |
sibling checkout | ls ../adhar-ui (or set ADHAR_UI_PATH) |
The fastest way to see it. Images publish to ghcr.io/adhar-io/adhar-console
and are mirrored to Docker Hub as adhario/adhar-console; GHCR packages start
private, so either make the package public or docker login ghcr.io first.
cp .env.example .env # fill it in — see the env table below
docker compose -f deploy/compose/docker-compose.yml up
# → http://localhost:3000 · /healthz /readyz /api/configEvery variable is listed in the .env table; for
the container set AUTH_PUBLIC_URL=http://localhost:3000.
docker compose brings its own Postgres. To run the image alone against an
existing database:
docker run --rm -p 3000:3000 --env-file .env ghcr.io/adhar-io/adhar-console:latest✅ You should see curl -s localhost:3000/readyz report ready, and
http://localhost:3000 redirect you to the Adhar-themed Keycloak sign-in above.
The server fails closed: without
KEYCLOAK_URL,AUTH_CLIENT_SECRETandAUTH_COOKIE_SECRETit refuses to boot rather than silently starting in an unauthenticated mode. That is deliberate — see docs/architecture/auth.md.
pnpm install
cp .env.example .env # then fill in the table below
pnpm dev # → http://localhost:5100pnpm dev starts 10 processes: the BFF (Deno server.ts, :5099), the Vite
host (:5100), and every federated remote (:5101–5108). The host proxies all
/api/* calls to the BFF, which does the real work against your cluster. Sign in
at http://localhost:5100 through the real Keycloak.
✅ You should see all 10 processes report listening, and signing in at
http://localhost:5100 land you on Overview with live cluster numbers. Empty
cluster pages mean your Keycloak user has no RBAC — see the table below.
Working on one phase? Start only what you need — the BFF and host always come along:
deno task dev develop platformEach of these has a comment in .env.example explaining where to get it.
| Variable | Dev value | How to get it |
|---|---|---|
KEYCLOAK_URL |
https://keycloak.adhar.localtest.me:8443 |
your platform's Keycloak |
KEYCLOAK_CLIENT_ID |
adhar-console |
pre-provisioned by the platform |
AUTH_CLIENT_SECRET |
— | kubectl -n adhar-system get secret keycloak-clients -o jsonpath='{.data.ADHAR_CONSOLE_CLIENT_SECRET}' | base64 -d |
AUTH_COOKIE_SECRET |
any random ≥32 chars | openssl rand -base64 48 |
AUTH_PUBLIC_URL |
http://localhost:5100 |
the origin your browser uses |
AUTH_COOKIE_SECURE |
false |
plain-HTTP localhost |
K8S_API_URL |
https://127.0.0.1:6443 |
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}' |
DENO_CERT |
path to a PEM bundle | must trust the apiserver and Keycloak CAs |
DATABASE_URL |
postgres://… |
port-forward console-db, or use the compose Postgres |
tool *_URL |
https://<tool>.adhar.localtest.me:8443 |
one per backing tool you want live |
The platform's
adhar-consoleKeycloak client already allows the dev redirecthttp://localhost:5100/api/auth/callback, so no Keycloak change is needed.
| Symptom | Cause | Fix |
|---|---|---|
| Server exits immediately on boot | Keycloak env missing | Set KEYCLOAK_URL, AUTH_CLIENT_SECRET, AUTH_COOKIE_SECRET — it fails closed by design |
| Login loops back to the sign-in page | AUTH_PUBLIC_URL doesn't match the origin in your address bar |
Make them identical, including port |
certificate / TLS errors in the BFF log |
DENO_CERT missing or incomplete |
Point it at a bundle trusting both the apiserver and Keycloak CAs |
| A tool's page says it can't be reached | That tool's *_URL is unset, so it resolves to a public hostname behind an OAuth proxy |
Set the in-cluster Service URL for that tool |
/readyz reports db: unconfigured |
No DATABASE_URL |
Preferences, notifications and the document store return 503 without it |
| Cluster pages are empty but tools work | Your Keycloak user has no RBAC in the cluster | The console impersonates you; bind your group in the cluster |
Offline / tests only: pass
mode:'stub'to a client factory for in-memory fixtures. There is no automatic stub fallback — a running system always talks to real backends.
adhar up already deploys the console as a platform package — see
Deploy on the Adhar platform below for the
manifests, and Enable SSO for the client it needs.
👉 The long form of all three paths — every variable, every prerequisite, and what to expect at each step — is docs/getting-started.md.
A production build is a Vite SPA served by the standalone Deno server. adhar-ui
is passed as a BuildKit build context rather than vendored, so the image build
needs it checked out alongside this repo.
docker build \
--build-context adhar-ui=../adhar-ui \
-f deploy/Dockerfile \
-t ghcr.io/adhar-io/adhar-console:dev .Without Docker:
pnpm build # → apps/console/dist/
cd apps/console
deno run -A --env-file=../../.env server.ts # → http://localhost:3000| Path | Purpose |
|---|---|
/ |
SPA (host + remotes, SPA-routing fallback) |
/api/auth/* |
OIDC login / callback / logout / session |
/api/k8s/* |
Per-user Kubernetes gateway (watch, log-follow, SSA); /api/k8s/exec (WebSocket) |
/api/svc/<tool>/… |
Authenticated reverse proxy to a backing tool |
/api/store/<kind>[/<id>] |
Console-owned document store (Postgres, tenant-scoped) |
/api/prefs/<s>, /api/notifications |
Postgres-backed user state |
/api/ai/* |
AI assistant (read-only tools + human-approved proposals) |
/api/config |
Non-secret runtime config for the browser |
/healthz /readyz |
Liveness / readiness probes |
Entities that no cluster resource or backing tool owns — OKRs, saved views,
custom roles, webhooks, design docs (ADRs/diagrams/…), API specs — persist in
Postgres via a generic, tenant-scoped store at /api/store/<kind>[/<id>]
(documents table: (tenant, kind, id) → jsonb). It's real and multi-user:
everyone in a tenant sees the same objectives. There is no localStorage or
in-memory fallback — a missing DB returns 503.
Modules use the docStore browser client (from @adhar/shell-ui):
import { docStore } from '@adhar/shell-ui'
await docStore.list('okr.objective') // → StoredDoc[]
await docStore.create('okr.objective', {...}) // server-assigned id
await docStore.put('design.adr', id, {...}) // upsert
await docStore.remove('workspace.webhook', id)The console is a confidential OIDC client. In production these are required — the server fails closed (refuses to boot) if Keycloak isn't configured, so it can never silently run in demo mode (see docs/architecture/auth.md):
KEYCLOAK_URL=https://keycloak.example.com KEYCLOAK_REALM=adhar
KEYCLOAK_CLIENT_ID=adhar-console # dedicated confidential client
AUTH_CLIENT_SECRET=<confidential client secret>
AUTH_COOKIE_SECRET=<random ≥32 chars> # openssl rand -base64 48
AUTH_PUBLIC_URL=https://console.example.com # external origin (redirect_uri base); required in prod
DATABASE_URL=postgres://user:pass@host:5432/db # required for prefs + the document storeOn the Adhar platform this client is provisioned automatically as adhar-console
(redirect …/api/auth/callback, access-token audience kubernetes + groups
claim for per-user cluster impersonation) — see the platform's
keycloak-config.yaml. DATABASE_URL is required for /api/prefs,
/api/notifications, and /api/store/*; without it those endpoints return
503 and /readyz reports db: unconfigured.
adhar up deploys the console from
platform/stack/packages/core/adhar-console/manifests/install.yaml, which
points at adhario/adhar-console and wires the Keycloak + database env. The
console's own reference manifests live in deploy/k8s/
(Deployment with startupProbe, Service, Ingress, RBAC, ConfigMap, Secret,
Crossplane Database claim / Postgres StatefulSet). See
deploy/README.md.
adhar-console/
├── apps/console/ # Vite SPA (MF host) + server.ts (standalone Deno server + BFF)
├── packages/
│ ├── auth/ # OIDC: client hooks + server-only code exchange/JWKS/cookies
│ ├── db/ # Drizzle ORM schema + Postgres client (server-only)
│ ├── api-clients/ # Typed clients for every backing tool (.stub() / .auto())
│ ├── shell-ui/ # AppShell, sidebar, topbar, brand marks, data components
│ ├── tenancy/ # Tenant context + scoping
│ ├── mf-utils/ # Federated remote loader (Suspense + ErrorBoundary)
│ ├── platform-info/ # Platform version, backing-tool registry, changelog, roadmap
│ ├── build-config/ # Shared Vite host/remote config + aliases
│ └── utils/ · tsconfig/ · eslint-config/
├── modules/ # Federated remotes — one per 6D phase + platform + workspace
├── deploy/
│ ├── Dockerfile # Deno builder → Deno runtime (SPA + standalone server)
│ ├── compose/ # Local console + Postgres
│ └── k8s/ # Deployment, Service, Ingress, RBAC, ConfigMap, DB
├── .github/workflows/ # ci.yml (validate) · release.yml (multi-arch → Docker Hub)
└── docs/ # Architecture & guides (links below)
Three workflows form a full release pipeline:
- CI (
ci.yml) — on push/PR: fmt · lint · type-check · test (reported, non-blocking) and a validation container build. - Version bump (
version-bump.yml) — manual dispatch. Pickpatch/minor/major(or an explicit version) and it bumpspackage.json, promotes theCHANGELOG[Unreleased]section, commits, and pushes the annotated tagvX.Y.Z. - Release (
release.yml) — fired by thev*tag: builds the multi-arch (amd64 + arm64) image and pushes it with:X.Y.Z,:X.Y,:latest,:sha-<short>tags plus SBOM + provenance to GHCR (ghcr.io/<owner>/adhar-console, always) and Docker Hub (only if its secrets are set), then cuts a GitHub Release with auto-generated notes and the image digest. Refuses to overwrite an already-published version.
- Actions → Version bump → Run workflow → choose the bump. That tags
vX.Y.Z. - The Release workflow builds, pushes to Docker Hub, and publishes the GitHub Release.
Re-publish an existing version without moving the tag via Actions → Release → Run workflow (enter the version).
The image publishes to GHCR by default with no setup — it authenticates with
the built-in GITHUB_TOKEN, so ghcr.io/<owner>/adhar-console just works. Docker
Hub is an optional mirror, enabled only when its secrets are present.
| Secret | Required? | Purpose |
|---|---|---|
| (none) | — | GHCR push uses the built-in GITHUB_TOKEN |
DOCKERHUB_USERNAME |
optional | Docker Hub org/namespace (adhario) — also mirrors the image there |
DOCKERHUB_TOKEN |
optional | Docker Hub access token (Read/Write) |
ADHAR_UI_TOKEN |
optional | PAT to check out adhar-io/adhar-ui if private |
RELEASE_PAT |
optional | PAT so the bump's tag auto-triggers Release (GitHub blocks GITHUB_TOKEN tag pushes from starting workflows) — without it, dispatch Release manually |
GHCR images start private. Make the package public (or grant pull access) under the org's Packages settings, or configure an
imagePullSecret.
Both build jobs check out adhar-io/adhar-ui as a sibling for the Docker build context.
| Guide | What it covers |
|---|---|
| 🚀 Getting Started | The long form of the three paths above, with every variable explained |
| 🏛️ Architecture | How the SPA, the federated remotes and the BFF fit together |
| 🧩 Module Federation | How a phase is built, loaded and owned independently |
| 🔌 BFF | Every /api/* surface and the token-injecting proxy |
| 🔐 Auth | The confidential OIDC client, sessions, and per-user cluster impersonation |
| 🏢 Tenancy | Tenant scoping and what is shared across an organisation |
| 🚢 Deploy · Observability | Shipping it, and watching it once shipped |
| ☸️ Kubernetes setup | Wiring the apiserver, OIDC audience and RBAC the console expects |
| 🤝 Contributing · Security · Changelog | Working on it, reporting issues, what changed |
Per phase: Define · Design · Develop · Deliver · Discover · Decide · Platform · Workspace
Related repos: adhar (the platform) · adhar-ai (the agentic layer) · adhar-ui (the design system)
Apache-2.0. See LICENSE. The backing open-source tools retain their own licenses — the status page lists each one.