KDCube

KDCube applications can now publish their normal ui.main_view as complete websites. One deployment may host several sites. Each site receives a stable alias; a domain can select it by host; one site can be the deployment's default. Files, directory indexes, and client-side routes all use the same application UI lifecycle.

The part that makes this safe at runtime is not a clever proxy. It is a small, versioned routing catalog: bundles.yaml remains the authority, Redis distributes catalog generations, and each proc answers requests from an immutable in-memory snapshot.

APPLICATION OWNS PRESENTATION · KDCUBE OWNS ROUTING Application source files (ui/site) bundles.yaml — site declaration main-view build owns presentation & composition KDCube application UI storage site catalog — revision + generation proc — immutable snapshot build · routing · storage · auth contract Browser / CDN /sites/{alias} custom domain — by host OpenResty · CDN forward no application registry here OpenResty forwards stable platform routes and the CDN caches responses — neither holds a list of applications; adding a site is a catalog operation.
The application owns presentation; KDCube owns build, routing, storage, and the auth contract; the proxy layer holds no application registry.

The application owns the site

Site registration lives with the application in bundles.yaml:

- id: website@2026-07-12
  name: Website
  singleton: false
  config:
    ui:
      main_view:
        src_folder: ui/site
        build_command: >-
          cp index.html site.js styles.css
          <VI_BUILD_DEST_ABSOLUTE_PATH>/
        site:
          enabled: true
          alias: workspace
          default: true
          hosts:
            - workspace.example.com
            - "*.workspace-preview.example.com"

          # The fields below belong to this particular website application.
          # The routing platform does not interpret them.
          title: KDCube Workspace
          scene_application_id: workspace@2026-03-31-13-36
Only four fields are the platform site contract.
FieldMeaning
enabledPublish this application's already-built main view as a site.
aliasUnique public key used by /sites/{alias}. _root is reserved.
defaultUse this site at / when no host declaration matches. At most one site may be default.
hostsExact domains or wildcard patterns that select this site before the default.

Everything else under site is application-owned composition. The reference website uses title and scene_application_id because its landing page hosts the Workspace scene; a docs site, portal, or product console can define different fields and expose its own public configuration API. This does not belong in assembly.yaml — assembly selects platform infrastructure, while the site follows the application's descriptor, release, source, build, and storage lifecycle.

Build a website the way you build a main view

The application entrypoint declares defaults and a normal public API. It does not hardcode its runtime application id — the actual id comes from the resolved application spec, so the source keeps no second BUNDLE_ID constant that can drift from the decorator or descriptor:

from typing import Any, Dict

from kdcube_ai_app.apps.chat.sdk.solutions.chatbot.entrypoint import BaseEntrypoint
from kdcube_ai_app.infra.plugin.bundle_loader import api, bundle_entrypoint, bundle_id


SITE_BUILD_COMMAND = "cp index.html site.js styles.css <VI_BUILD_DEST_ABSOLUTE_PATH>/"


@bundle_entrypoint(name="website", version="2026.07.12", priority=10)
@bundle_id(id="website@2026-07-12")
class WebsiteEntrypoint(BaseEntrypoint):
    def configuration_defaults(self) -> Dict[str, Any]:
        return {
            "ui": {
                "main_view": {
                    "src_folder": "ui/site",
                    "build_command": SITE_BUILD_COMMAND,
                    "site": {
                        "enabled": False,
                        "alias": "workspace",
                        "default": False,
                        "hosts": [],
                    },
                },
            },
        }

    @api(method="GET", alias="site_config", route="public")
    async def site_config(self, **kwargs: Any) -> Dict[str, Any]:
        del kwargs
        identity = self.runtime_identity()
        spec = getattr(self.config, "ai_bundle_spec", None)
        site = self.bundle_prop("ui.main_view.site", {}) or {}
        return {
            "application_id": str(getattr(spec, "id", None) or "").strip(),
            "site_alias": str(site.get("alias") or "").strip(),
            "title": str(site.get("title") or "KDCube").strip(),
            "tenant": str(identity.get("tenant") or "").strip(),
            "project": str(identity.get("project") or "").strip(),
            "platform_config_url": "/api/cp-frontend-config",
            "profile_url": "/profile",
        }

For a multipage build, copy or compile the complete output tree into application UI storage with the normal build placeholder:

ui/site/
  index.html
  styles.css
  assets/
    logo.8f2a91.svg
    app.725ad1.js
  guide/
    index.html
  pricing/
    index.html

A Vite, React, Next static export, or another frontend build can replace the small cp example; the site router does not depend on the build tool.

One declaration becomes three addresses

An enabled site has a direct alias even when no custom domain exists:

/sites/workspace
/sites/workspace/guide/
/sites/workspace/assets/app.725ad1.js

The deployment root resolves by host first, then by the one explicit default:

request /
   |
   +-- Host matches site.hosts? ----------> matching site
   |
   +-- otherwise one default: true? ------> default site
   |
   +-- otherwise -------------------------> configured platform chat

A CDN receives one additional stable origin surface for clean domain paths — /api/integrations/site-root/{path}.

The public URL stays clean. site-root is an origin contract, not the URL a reader should bookmark.

Why routing does not read Redis on every request

The first prototype could resolve a site by reading the active application registry and every application's properties on each request. It worked, but it put descriptor authority and Redis on the website's hot path. The implemented model follows the same separation used elsewhere in KDCube: authority, distributed projection, hot serving state.

CATALOG LIFECYCLE: CHANGE LANE vs REQUEST LANE CHANGE PATH bundles.yaml configuration authority validate enabled site declarations ApplicationSiteCatalog revision — content id generation — update order atomic Redis script INCR generation SET snapshot PUBLISH event proc subscribers NO REDIS READ · NO YAML PARSE · NO APPLICATION SCAN ON THE REQUEST PATH load snapshot once, on event REQUEST PATH browser / CDN clean URL stable proxy OpenResty /api forward immutable proc-local catalog host / alias lookup in memory UI storage built site files 200 The revision names the routing state; the generation orders distributed updates — an older generation is rejected, so a delayed event never rolls a worker back. An invalid update is reported and dropped; the previous valid catalog keeps serving.
Change lane and request lane, separated by one rule: the request path never reads Redis, parses YAML, or scans applications.

The catalog carries both a revision and a generation. The revision is derived from catalog content — is this the same routing state? The generation is assigned monotonically by Redis — which distributed update is newer? Each proc subscribes before loading the current snapshot, closing the usual pub/sub catch-up gap, and a delayed event cannot roll a worker backward because older generations are rejected.

Publication is one atomic Redis step: increment the generation, replace the complete snapshot, and publish the event. The catalog also carries the application's resolved runtime target (path, module, singleton) — so when an application comes from Git, routing and the application build arrive as one coherent generation instead of depending on two independently timed registries.

Invalid routing never becomes the new routing

Catalog compilation rejects ambiguous declarations before publication:

  • an alias is missing, malformed, duplicated, or reserved;
  • more than one site is the default;
  • two sites declare the same host;
  • exact and wildcard hosts overlap;
  • two wildcard domains can match the same request.
valid catalog N (serving)
        |
        +-- descriptor update --> validate
                                  |
                                  +-- valid ----> publish N+1
                                  |
                                  +-- invalid --> report error
                                                  keep N serving

The platform does not choose an arbitrary winner. During a live invalid update, the previous valid catalog remains hot. On a fresh deployment with no valid catalog, site routing is unavailable until the declaration is corrected.

Multipage files and SPA routes share one surface

The static lifecycle resolves a requested path in this order:

requested path
   |
   +-- existing file --------------------> serve file
   |
   +-- existing directory --------------> serve directory/index.html
   |
   +-- missing path + root index exists -> serve root index.html (SPA fallback)
   |
   +-- no root index --------------------> 404

That lets a single mechanism host a traditional generated multipage site, a client-routed React application, a hybrid of generated pages and client-side sections, or a shell that hosts one or more KDCube scenes. Every HTML response receives a clean public <base> and a small kdcube-site-context JSON block:

{
  "schema_version": 1,
  "tenant": "demo-tenant",
  "project": "demo-project",
  "application_id": "website@2026-07-12",
  "site_alias": "workspace",
  "public_base": "/sites/workspace/",
  "catalog_revision": "..."
}

Browser code reads this context instead of reverse-engineering tenant, project, and application id from an internal API URL — so the same source works through an alias and through a clean custom domain.

A CDN forwards; it does not become the registry

Custom domains use the same idea as KDCube public-content delivery: preserve the reader's URL, rewrite only the origin request, and let the platform serve the authoritative result.

A CDN FORWARDS — IT DOES NOT BECOME THE REGISTRY READER URL, UNCHANGED https://docs.example.com/guide/ Reader requests the clean custom-domain URL Site CDN preserve Host / X-Forwarded-Host rewrite the origin request only KDCube OpenResty stable /api forward no site list proc hot catalog by host: docs.example.com → docs-site origin only: /api/integrations/site-root/guide/ — not a reader URL RESPONSE proc reads guide/index.html from UI storage → 200 HTML → CDN caches → returns to the reader (URL still docs.example.com/guide/)
The reader URL never changes; only the origin request is rewritten, and proc selects the site from its hot catalog by host.

The hosts declaration does not provision DNS, certificates, or a CDN distribution — deployment still owns those resources. It configures the domain to reach the KDCube runtime, preserves the viewer host, and rewrites /<path> to /api/integrations/site-root/<path>. Neither the CDN nor OpenResty contains a list of applications: OpenResty forwards stable platform routes, and adding, removing, or remapping a site is a descriptor/catalog operation, not a proxy regeneration.

Cache the right layer

Application sites use the existing main-view build and static cache policy — there is no reason to query Redis for every asset, and no reason to register the website shell as public content merely to obtain CDN caching.

MaterialOwner and tierCache behavior
Site declarationbundles.yamlAuthoritative configuration
Site routing projectionRedis snapshot + generationRebuildable distribution state
Request routing catalogProc memoryImmutable hot state
Website sourceApplication packageSource control and release
Built websiteApplication UI storageRebuildable serving cache
HTML / root non-hashed filesProc/CDN responseno-cache, ETag revalidation
Nested non-hashed filesProc/CDN responseBounded cache
Content-hashed assets/ filesProc/CDN responseOne year, immutable

Website delivery is not @public_content

The two mechanisms can appear on the same domain, but they solve different jobs:

Application-hosted websitePublic content
Complete main-view file treePublished content records
HTML, JS, CSS, images, routesArticles, metadata, catalogs
Alias + host + default selectionPublic slug + content alias
Multipage and SPA fallbackSitemap + JSON-LD + canonical page
Application UI build / storageDurable registry + hot content tier

Use application-hosted sites for the shell and pages of a complete web experience. Use @public_content when an application publishes independently indexed records that need catalogs, sitemaps, structured metadata, canonical URLs, and explicit publish/retract semantics. A website can link to public-content pages; it does not need to absorb their storage model, and public content does not need to become a website build.

Authentication remains a platform concern

A website should not know whether this deployment uses Cognito, multiple Cognito providers, or an application-hosted platform authority.

  • GET /api/cp-frontend-config — the active browser auth contract.
  • GET /profile — authoritative session state.
  • configured login / logout URLs.
  • application APIs and scenes — the same-origin platform session.

The reference website reads platform configuration from /api/cp-frontend-config and treats /profile as truth. It can host the Workspace scene and relay the standard scene configuration and authentication-change messages without embedding provider-specific login code. This is another reason to keep site selection out of assembly and proxy files: the application owns presentation; the platform owns authority and session semantics.

Build and verify one end to end

  • Create an application with a built ui.main_view.
  • Add ui.main_view.site to its bundles.yaml entry.
  • Give it a unique alias.
  • Optionally declare hosts and one deployment default.
  • Refresh/rebuild KDCube so the platform and UI build are current.
  • Verify the alias, a real file, a directory index, and an SPA path.
  • Verify /profile before and after login.
  • For a custom domain, configure the CDN host/path rewrite and test the forwarded host.
  • Inspect cache headers for HTML and a hashed asset.
  • Change a site declaration and verify a new catalog generation is applied without changing OpenResty.
# Alias root
curl -I http://localhost:<proxy-port>/sites/workspace

# Multipage path
curl -I http://localhost:<proxy-port>/sites/workspace/guide/

# Unknown alias
curl -I http://localhost:<proxy-port>/sites/does-not-exist

# Simulate the CDN origin request for a configured host
curl -I \
  -H 'Host: workspace.example.com' \
  http://localhost:<proxy-port>/api/integrations/site-root/guide/

Expected: the enabled alias and configured host routes return the application site; an unknown alias returns 404; ambiguous configuration is rejected instead of choosing a site; HTML revalidates; hashed assets are immutable; and the platform UI and /api/* routes keep their normal owners.

KDCube Deep · 12.07.2026