Your KDCube Application Can Be a Website
An application already owns a browser experience: source files, a build, a main view, APIs, and the platform session around them. It should not need a second web server, a hand-written reverse-proxy block, or a separate authentication implementation to become a website. Declare the site in the application; KDCube builds it, routes it, and gives a CDN one stable origin contract.
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.
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
| Field | Meaning |
|---|---|
enabled | Publish this application's already-built main view as a site. |
alias | Unique public key used by /sites/{alias}. _root is reserved. |
default | Use this site at / when no host declaration matches. At most one site may be default. |
hosts | Exact 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.
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.
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.
| Material | Owner and tier | Cache behavior |
|---|---|---|
| Site declaration | bundles.yaml | Authoritative configuration |
| Site routing projection | Redis snapshot + generation | Rebuildable distribution state |
| Request routing catalog | Proc memory | Immutable hot state |
| Website source | Application package | Source control and release |
| Built website | Application UI storage | Rebuildable serving cache |
| HTML / root non-hashed files | Proc/CDN response | no-cache, ETag revalidation |
| Nested non-hashed files | Proc/CDN response | Bounded cache |
Content-hashed assets/ files | Proc/CDN response | One year, immutable |
Website delivery is not @public_content
The two mechanisms can appear on the same domain, but they solve different jobs:
| Application-hosted website | Public content |
|---|---|
| Complete main-view file tree | Published content records |
| HTML, JS, CSS, images, routes | Articles, metadata, catalogs |
| Alias + host + default selection | Public slug + content alias |
| Multipage and SPA fallback | Sitemap + JSON-LD + canonical page |
| Application UI build / storage | Durable 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.siteto itsbundles.yamlentry. - 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
/profilebefore 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.