Skip to content
DIH Knowledge Base
    Sign in

    Architecture & technical setup

    This page is the narrative architecture guide for the DIH Knowledge Base PoC: Astro Starlight on SSR, Keycloak OIDC, product roles in the JWT, and what we deliberately do not ship compared to docs.dih.telekom.com.

    Runtime

    Astro 6 + Starlight 0.39, output: 'server', @astrojs/node standalone — every doc request can read cookies and redirect.

    Identity

    Keycloak authorization code + PKCE; access JWT in an HttpOnly cookie; logout via id_token_hint.

    Authorization

    Frontmatter-driven: requiresAuth, requiredRoles (WIKIJS_* groups) — three enforcement layers.

    Locales

    /en/ and /de/ content trees; / redirects to /en/ — aligned with production URL shape.

    Every documentation URL passes through Astro middleware, then Starlight route middleware, then our custom page frame (header, sidebar, content or upsell).

    flowchart TB
    subgraph Browser["Reader browser"]
      REQ["GET /en/products/..."]
    end
    subgraph Layer1["Layer 1 — src/middleware.ts"]
      JWT["Read dih_access_token cookie"]
      REDIR["Redirect to /auth/login?"]
      ACC["Set locals.pageAccess"]
    end
    subgraph Layer2["Layer 2 — src/route-middleware.ts"]
      SB["Filter sidebar by roles"]
    end
    subgraph Layer3["Layer 3 — AuthPageFrame.astro"]
      UP["UpsellScreen"]
      MDX["MDX page body"]
    end
    REQ --> JWT
    JWT -->|invalid / missing + requiresAuth| REDIR
    JWT --> ACC
    ACC --> SB
    SB -->|upsell| UP
    SB -->|allowed / public| MDX
    SSR request pipeline — auth before Starlight renders the page
    1. Browser requests a doc URL (/en/… or /de/…).
    2. Layer 1 validates the JWT, maps the path to a content entry, redirects to Keycloak if login is required, or sets pageAccess to public, allowed, or upsell.
    3. Layer 2 rebuilds the sidebar so navigation only lists pages the user may open (same rules as search).
    4. Layer 3 renders either the MDX body or an in-page upsell (URL unchanged — no body leak).
    LayerChoiceRole
    FrameworkAstro 6SSR, content collections, API routes
    Docs UIStarlight 0.39Sidebar, MDX, i18n, component overrides
    Runtime@astrojs/node (standalone)node ./dist/server/entry.mjs in K8s
    AuthKeycloak + joseOIDC, JWKS validation
    Searchpublic/search-index.jsonBuilt at dev/build; filtered in header (not Pagefind)
    StylingTeleNeo Web + custom CSSTelekom chrome; no ODS React runtime

    Sign-in is OIDC authorization code with PKCE. Auth routes live at the site root (/auth/…) so the same flow works from /en/ and /de/.

    sequenceDiagram
    participant U as "Browser"
    participant D as "Docs (Astro SSR)"
    participant K as "Keycloak (iam.dev.dih-cloud.com)"
    
    U->>D: GET /en/guides/platform-overview/
    D->>U: 302 /auth/login?returnTo=...
    U->>D: GET /auth/login
    Note over D: Store PKCE verifier, state, returnTo
    D->>U: 303 authorize URL (PKCE)
    U->>K: User signs in (SSO)
    K->>U: 302 /auth/callback?code=...
    U->>D: GET /auth/callback
    D->>K: POST token (code + verifier)
    K-->>D: access_token, id_token
    Note over D: Set dih_access_token + dih_id_token cookies
    D->>U: 302 returnTo (original page)
    U->>D: GET original page (JWT in cookie)
    Protected page → Keycloak → callback → session cookies → original URL
    sequenceDiagram
    participant U as "Browser"
    participant D as "Docs"
    participant K as "Keycloak"
    
    U->>D: GET /auth/logout
    Note over D: Clear session cookies
    D->>U: 302 Keycloak end-session
    Note over D,K: id_token_hint + post_logout_redirect_uri
    K->>U: End SSO session
    K->>U: 302 http://localhost:4321/en/
    Logout clears cookies and ends the Keycloak SSO session
    1. User clicks Sign outGET /auth/logout.
    2. App deletes dih_access_token (and related) cookies.
    3. Browser is redirected to Keycloak end-session with id_token_hint from dih_id_token.
    4. Keycloak redirects to KEYCLOAK_POST_LOGOUT_REDIRECT_URI (default http://localhost:4321/en/ — must be registered on the client).
    RoutePurpose
    /auth/loginStart OIDC; stores PKCE verifier, state, returnTo
    /auth/callbackExchange code; set session cookies
    /auth/logoutClear cookies; Keycloak end-session

    Session cookie — access JWT (SESSION_COOKIE_NAME, default dih_access_token).

    Logout cookieid_token (dih_id_token) for id_token_hint.

    Roles are resolved from ROLE_CLAIM with fallbacks (groups, realm_access.roles, …). Mapping table: Access matrix.

    .env (excerpt)
    KEYCLOAK_ISSUER=https://iam.dev.dih-cloud.com/realms/dih
    KEYCLOAK_CLIENT_ID=astro-starlight
    KEYCLOAK_CLIENT_SECRET=***
    KEYCLOAK_REDIRECT_URI=http://localhost:4321/auth/callback
    APP_BASE_URL=http://localhost:4321
    KEYCLOAK_POST_LOGOUT_REDIRECT_URI=http://localhost:4321/en/

    Access is declarative in frontmatter — no separate permissions database in the PoC.

    flowchart TD
    A["Incoming request"] --> B{"Public page?<br/>no requiresAuth / roles"}
    B -->|yes| Z["Render MDX content"]
    B -->|no| C{"Valid JWT?"}
    C -->|no| L["302 → /auth/login"]
    C -->|yes| D{"requiredRoles<br/>in token?"}
    D -->|yes| Z
    D -->|no| U["UpsellScreen<br/>(URL unchanged)"]
    Decision tree implemented in src/lib/access.ts + middleware

    File: src/middleware.ts

    • Skips /_astro/, /auth/, /health, static files with extensions
    • verifyAccessTokenAstro.locals.user
    • pathnameToDocId → content entry for /en/… and /de/…
    • requiresAuth without user → redirect to login with returnTo
    • Sets Astro.locals.pageAccess via toStarlightPageAccess
    src/lib/access.ts — core decision
    export function getPageAccess(
    data: DocAccessFields,
    user?: User,
    ): PageAccessResult {
    if (!pageNeedsAuth(data)) return 'public';
    if (!user) return 'auth_required';
    if (data.requiredRoles?.length && !userHasRequiredRole(data, user)) {
    return 'upsell';
    }
    return 'allowed';
    }
    Public guide — no gates
    title: About DIH
    description: Platform introduction
    # No requiresAuth / requiredRoles

    Production uses /en and /de without bare localized content at /. This PoC mirrors that.

    flowchart LR
    ROOT["GET /"] --> EN["/en/ home"]
    EN --> ENC["content/docs/en/"]
    DE["/de/ home"] --> DEC["content/docs/de/"]
    ENC -.->|fallback| DEC
    Parallel locale trees; German pages fall back to English when missing
    LocaleContent folderExample URL
    English (default)src/content/docs/en//en/poc/architecture/
    Deutschsrc/content/docs/de//de/poc/architecture/
    • defaultLocale: 'en' and redirects: { '/': '/en/' } in astro.config.mjs
    • UI copy: src/lib/ui-copy.ts · German sidebar: src/lib/sidebar-labels.ts

    We override Starlight primitives in astro.config.mjs instead of forking the theme.

    OverrideFileBehaviour
    PageFrameAuthPageFrame.astroUpsell shell + DIH footer
    HeaderHeader.astroTelekom T, brand, nav, DocsSearch, login, language
    HeroHero.astro + DihLandingHero.astroLanding imagery on /en/ and /de/
    SidebarSidebar.astroMobile footer in drawer
    ThemeProviderThemeProvider.astroLight-only (no dark toggle)

    Global flags: prerender: false, pagefind: false, routeMiddleware: './src/route-middleware.ts'.

    Custom Astro

    MermaidDiagram, WorkshopEmbed, DocDownload, UpsellScreen — see Custom components.

    flowchart LR
    BUILD["npm run dev / build"] --> IDX["generate-search-index.mjs"]
    IDX --> JSON["public/search-index.json"]
    JSON --> UI["DocsSearch in Header"]
    UI --> FILT["search-access.ts filters by roles"]
    Static index + client-side role filter (not Starlight Pagefind)

    Starlight Pagefind is off because it conflicts with full SSR. Instead:

    1. scripts/generate-search-index.mjs walks the docs collection and writes titles, descriptions, URLs, requiresAuth, requiredRoles.
    2. src/integrations/search-index.ts runs the script on dev and build.
    3. DocsSearch loads the JSON and hides hits the current user cannot access (mirrors sidebar rules).
    High-level tree
    src/
    content/docs/en/ # English MDX/Markdown
    content/docs/de/ # German (partial)
    components/ # Header, Hero, Upsell, MermaidDiagram, …
    lib/ # access, keycloak, jwt, paths, locale, search-access
    middleware.ts # Layer 1
    route-middleware.ts # Layer 2
    pages/auth/ # login.ts, callback.ts, logout.ts
    public/
    specs/ # OpenAPI 3 + Swagger 2.0 for Swagger UI embed
    downloads/ # CSV templates, Postman collection
    search-index.json # generated — do not hand-edit

    Extended schema in src/content.config.ts: requiresAuth, requiredRoles, upsellTitle, upsellCtaLabel, upsellCtaUrl.

    flowchart TB
    subgraph CI["GitLab CI / local"]
      GIT["git push"]
      BUILD["docker build"]
    end
    subgraph K8s["Kubernetes (target)"]
      POD["Pod: Node 22"]
      POD --> ENTRY["node dist/server/entry.mjs"]
    end
    subgraph IAM["Identity"]
      KC["Keycloak realm"]
    end
    GIT --> BUILD --> POD
    User["Reader"] --> POD
    POD --> KC
    Multi-stage Dockerfile → standalone Node server → OIDC to Keycloak
    1. Buildnpm run build (Node ≥ 22.12); outputs dist/server/ + client assets.
    2. ImageDockerfile multi-stage; CMD runs node ./dist/server/entry.mjs.
    3. ProbeGET /health returns OK (excluded from auth middleware).
    4. Configure — inject KEYCLOAK_*, APP_BASE_URL, allowed redirect/logout URIs on the Keycloak client.
    Terminal window
    npm install
    cp .env.example .env # fill Keycloak secrets
    npm run dev # http://localhost:4321 → /en/

    Platform fit

    Starlight as the docs shell with SSR, bilingual URLs, and Telekom chrome — ready for architecture reviews and workshops.

    Auth model

    End-to-end OIDC, JWT session, three-layer gating mapped to WIKIJS_* IAM groups.

    Authoring depth

    Representative SaaS doc patterns: structure, components, code, diagrams, API embed, static downloads.

    Deploy path

    Node adapter + Docker as a starting point for Kubernetes — not a full platform ops kit.

    TopicStatus
    Full production content migrationOut of scope — demo copy only
    Wiki.js / CMS authoringOut of scope — git-based MDX only
    Starlight PagefindDisabled — SSR + auth conflict
    Private search metadataPoC gap — see note above
    ODS React runtimeRemoved — CSS + custom components
    Audit logging, rate limits, WAFNot implemented
    Automatic IAM / group provisioningManual Keycloak test users
    SEO, analytics, cookie consentNot configured
    HA, CDN, preview environmentsSingle image / single region assumed
    In-app content workflowGit PR review only

    Production target: docs.dih.telekom.com remains the live knowledge base; this repo answers can we rebuild the experience on Starlight with the same auth and URL model?