KDCube
← Engineering
KDCube Engineering · Deep Dive

Your App Has a Frontend. Why Deploy It Twice?

The app already has HTML, JavaScript, APIs, authentication, and a release. KDCube publishes its existing main view as a complete website without creating a second frontend artifact.

2026-07-12Engineering15 minExperience
application websites app frontend site routing local deployment self-hosted app website multiple websites one runtime ui.main_view site

Deploying the frontend again as a separate website creates a second build, another proxy contract, and another place for runtime configuration to drift. KDCube can publish the app's existing main view as a complete website without creating a second frontend artifact.

An app in KDCube can contain an agent, REST APIs, MCP services, named services, jobs, event handlers, widgets, and a complete browser experience. The website does not sit beside that app as another product. It is one of the app's surfaces, built and released with the rest of it.

The smallest useful change is one site declaration below the existing ui.main_view. That registration gives the same built file tree a stable /sites/{alias}/ address. It can also own a clean hostname root or become the one default root for the installation.

One app can contribute one site. One KDCube installation can load many apps, so it can serve many independently built websites while the control plane, APIs, MCP, streaming, and sockets keep their own routes.

00 Implementation highlights

ConceptThe takeaway
One build, two rolesui.main_view builds the app frontend; ui.main_view.site registers that same artifact as a website.
Alias firstEvery enabled site works at /sites/{alias}/ without DNS, TLS, or a custom domain.
Many sitesOne app contributes at most one site, while one installation serves many sites by loading many apps.
Clean rootshosts selects a site for a matching request host; one default: true site handles unmatched hosts.
Local to publicThe same app declaration works through the local proxy, a tunnel, Caddy, or a cloud edge that preserves the request host.
Stable platform routesThe control plane and service routes remain reserved while root and other clean paths belong to the selected site.
Hot routingApp configuration is projected into a versioned catalog; requests resolve an immutable proc-local snapshot.

01 The smallest honest website declaration

Assume an app already builds a frontend:

bundles.yaml · main viewYAML
- id: docs@1-0
  config:
    ui:
      main_view:
        src_folder: ui/site
        build_command: >-
          npm run build -- --outDir <VI_BUILD_DEST_ABSOLUTE_PATH>

That is a main view. KDCube builds it, stores the active artifact, and serves it through app-scoped static routes. The control plane or another browser surface can open it, but it is not yet an installation-wide website.

Register the same artifact as a site by adding four routing fields:

bundles.yaml · site registrationYAML
- id: docs@1-0
  config:
    ui:
      main_view:
        src_folder: ui/site
        build_command: >-
          npm run build -- --outDir <VI_BUILD_DEST_ABSOLUTE_PATH>
        site:
          enabled: true
          alias: docs
          default: false
          hosts:
            - docs.example.com
            - "*.docs-preview.example.com"
FieldWhat it controls
enabledWhether the already-built main view participates in website routing.
aliasIts unique installation-wide address under /sites/{alias}/. _root is reserved.
defaultWhether this site handles / and clean paths when no host declaration matches. At most one site may be default.
hostsExact or wildcard request hosts that select this same site before the default.

Other fields may live under site, but they are app-owned composition data. The platform routing contract interprets only these four.

Site registration belongs to the app's bundles.yaml entry. assembly.yaml owns installation concerns such as the control-plane route prefix; it does not select which app becomes a website.

THE DISTINCTION

ui.main_view defines and builds one app frontend artifact. ui.main_view.site registers that artifact in the installation-wide website catalog and assigns alias, host, and optional default-root behavior.

Disabling or omitting site removes website routing. It does not delete the main view or its app-scoped routes. Enabling site does not start another build and does not store another copy.

ONE BUILD, TWO ADDRESSING ROLES The warm light marks the frontend artifact you already built. YOUR APP ui/site source HTML · JS · CSS app configuration ui.main_view source folder build command atomic activation ONE ACTIVE ARTIFACT one built file tree one app identity one release lifecycle APP-SCOPED ROUTES authenticated static route public app shell route OPTIONAL ui.main_view.site always: /sites/{alias}/ optional: matching-host / optional: one default / routing metadata only · no second build Disabling site registration removes website addresses; the main-view artifact and app-scoped routes remain.
Fig. 1 — Build the frontend once; choose where the same artifact may be addressed.

02 One artifact, four ways to reach it

The app-scoped addresses exist because the artifact belongs to an app:

authenticated app static route
  /api/integrations/static/{tenant}/{project}/{app-id}/...

public app shell route
  /api/integrations/bundles/{tenant}/{project}/{app-id}/public/static/...

Site registration adds three reader-facing roles.

A stable alias. Every enabled site receives an address independent of the request host:

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

This is the immediate local result. No domain setup is required.

A host-selected clean root. If Host: docs.example.com reaches KDCube and that host appears in the app's hosts, the site owns /, /guide/, and its other clean paths.

The default clean root. If no host-specific site matches, the one site with default: true owns those clean paths.

Selection is deterministic:

request for / or another clean path
              |
              v
does Host match an enabled site's hosts?
       | yes                         | no
       v                             v
matching site             is one site default: true?
                                  | yes          | no
                                  v              v
                            default site    root redirects to
                                            the control plane;
                                            other clean paths 404

An alias always selects one declared site directly. Host and default selection apply only to root and clean-path routing.

03 Many websites live beside many app capabilities

The current cardinality is simple:

one app
  +-- zero or one effective ui.main_view
  +-- optional zero or one @ui_main declaration
  `-- zero or one ui.main_view.site registration

one KDCube installation
  +-- app A -> zero or one website
  +-- app B -> zero or one website
  `-- app N -> zero or one website

A configuration-backed main view does not require @ui_main. If an app uses that optional code surface, the loader permits at most one. Several hosts entries do not create several websites; they are several names for the same app, alias, configuration, and built file tree.

Use separate apps when sites need independent source, configuration, release, ownership, or lifecycle. Each app can still provide other surfaces around its site.

MANY APPS, MANY SITES, ONE INSTALLATION Each app chooses its own surface mix; website registration is optional. ONE KDCUBE INSTALLATION DOCS APP website API MCP /sites/docs/ docs.example.com/ one source · one release WORKSPACE APP website agent UI /sites/workspace/ workspace.example.com/ chat · streaming · widgets OPERATIONS APP MCP jobs named svc no website registered service routes only still a complete KDCube app RESERVED ROUTES /platform/* /api/* /sse/* /socket.io/* auth · monitoring A website is one app surface. Apps without websites keep every other declared capability.
Fig. 2 — A website is one app surface, not a second deployment unit.

An enabled site requires a non-root control-plane mount such as /platform or /control/ui. The control plane and a host-selected website cannot both own the same root path, so proxy.route_prefix: / plus an enabled site is rejected as an invalid configuration.

04 The browser build must be address-portable

The same files may be served below an alias or at a clean root. The frontend therefore cannot assume that /assets/app.js always starts at the deployment root.

For Vite, use a relative base:

vite.config.jsJAVASCRIPT
export default {
  base: './'
}

Build the complete output tree into <VI_BUILD_DEST_ABSOLUTE_PATH>:

dist/
  index.html
  assets/
    app.725ad1.js
    logo.8f2a91.svg
  guide/
    index.html
  pricing/
    index.html

KDCube serves an existing file directly, resolves a directory to its index.html, and falls back to the root index.html for an unknown browser route when that shell exists. Traditional multipage output, a browser-routed SPA, and a hybrid site use the same surface.

For HTML responses, KDCube injects the route-appropriate <base> and a kdcube-site-context JSON block containing the app identity, site alias, public base, tenant/project scope, and catalog revision. The browser does not need to reverse-engineer those values from an internal URL.

The site should also use the platform's browser contracts:

/api/cp-frontend-config  -> active route and provider-neutral auth config
/profile                 -> authoritative browser-session state
configured login/logout  -> the deployment's active authentication flow
app APIs and scenes      -> the same-origin platform session

A public website shell does not make its data or actions public. Every app API, MCP call, file operation, event, and widget keeps its own authentication and authorization policy.

05 Local first, then choose the public topology

kdcube init gives the installation one local web origin. An enabled site is immediately testable by alias through that origin:

http://127.0.0.1:<proxy-port>/sites/docs/

If the site is the default, the same proxy can serve it at /. A local host entry can test host selection without public DNS:

host-selection checkSHELL
curl -H 'Host: docs.local.test' \
  http://127.0.0.1:<proxy-port>/

From there, three publication shapes cover the common cases.

Publish the complete KDCube origin

Point a tunnel, reverse proxy, or load balancer at the KDCube web-proxy port. The public origin retains the same route families:

https://runtime.example.net/platform/chat
https://runtime.example.net/sites/docs/
https://runtime.example.net/sites/workspace/

One public hostname does not automatically create subdomains. To give a site a clean custom root, route that hostname to KDCube, preserve Host, and list the hostname in site.hosts.

Keep another website at /

An outer Caddy or equivalent adapter can keep a separately deployed website at the public root and forward KDCube's reserved paths:

public hostname
  +-- /                         -> separate website files
  +-- /platform/*              -> KDCube control plane
  +-- /api/* and transports    -> KDCube services
  `-- /sites/*                 -> KDCube application-site aliases

In this composition, the separate website deliberately owns /. KDCube sites remain available by alias. A KDCube site needs another hostname routed wholly to KDCube when it must own a clean root too.

Path forwarding must carry the public origin through every trusted proxy hop. For example, when ngrok terminates HTTPS before a local Caddy HTTP listener, Caddy trusts only the loopback ngrok peer and preserves its public scheme:

browser --https--> ngrok --http + scheme:https--> Caddy
        --http + preserved scheme:https--> KDCube web proxy

KDCube separately validates that forwarded scheme under its descriptor-owned proxy policy. If either boundary is absent, the website may still render while request-derived callback, consent, download, or upload URLs are minted with the inward http scheme.

Use dedicated site domains

Several domains may reach one KDCube origin:

docs.example.com --------+
workspace.example.com ---+--> edge or tunnel --> KDCube web proxy
status.example.com ------+          preserve Host and scheme

KDCube selects the app after the request arrives. site.hosts does not create DNS records, certificates, tunnel endpoints, or CDN behaviors. The outer deployment owns those resources and forwards the clean request path. The generated OpenResty route matrix performs the internal clean-path rewrite.

ONE SITE DECLARATION, THREE PUBLICATION TOPOLOGIES The outer ingress moves requests; KDCube still selects and serves the app. A · COMPLETE KDCUBE ORIGIN browser or tunnel KDCube web proxy /platform · /api · /sites · host/default root aliases now · clean root when selected one origin owns the route matrix B · COMPOSED ROOT browser Caddy / outer adapter routes + trusted origin / -> separate files reserved paths -> KDCube /platform · /api · /sites/* aliases stay public C · DEDICATED SITE DOMAINS docs.example.com workspace.example.com edge / tunnel preserve Host + scheme KDCube catalog Host selects app site clean site root same app artifact Deployment owns DNS and TLS; every trusted hop preserves the public origin; KDCube selects the app.
Fig. 3 — The app declares the site; the outer ingress decides which host reaches the installation, and every trusted hop preserves the same public origin.

The app contract is the same locally and in production. Production adds the deployment's normal DNS, TLS, edge, and storage configuration; it does not add another app-specific frontend server.

06 Reload the app, not a hand-written proxy map

The app's source, build command, site declaration, and other surfaces move together through its normal lifecycle. When the app comes from Git, the website rides the same source and release path as the app APIs and agents:

source or build-affecting app props change
  -> app deploy/reload lifecycle
  -> main-view signature check
  -> build and atomically activate UI artifacts when needed
  -> reconcile the site catalog from current app configuration
  -> new alias/host/default routing becomes active

A site-only change does not require application-specific OpenResty edits. The proxy forwards stable route families; it never stores a list of site-owning apps.

The catalog rejects ambiguous states before publication:

  • a missing, malformed, duplicate, or reserved alias;
  • more than one default site;
  • duplicate host declarations;
  • overlapping exact and wildcard host patterns;
  • overlapping wildcard patterns that could select more than one site.

During a live invalid update, the previous valid catalog continues serving. On a fresh start with no valid catalog, site routing stays unavailable until the declarations are corrected. KDCube does not select an arbitrary winner.

07 The request path does not scan app configuration

bundles.yaml remains the authority, but parsing every app declaration on every HTML or asset request would put configuration and distributed storage on the website's hot path.

KDCube separates the change path from the request path:

                         CHANGE PATH

effective app configuration
  -> validate all enabled site declarations
  -> build ApplicationSiteCatalog
       revision: identity of this catalog content
       generation: distributed update order
  -> atomic Redis generation + complete snapshot + update event
  -> each proc replaces its immutable local catalog

                         REQUEST PATH

browser
  -> generated OpenResty route matrix
  -> proc-local immutable catalog
  -> alias or Host/default lookup
  -> active app UI storage
  -> HTML, asset, directory index, or SPA shell

No descriptor parse. No Redis read. No app scan.
SITE CATALOG: CHANGE LANE AND REQUEST LANE CHANGE PATH effective app config bundles.yaml authority enabled site declarations validate aliases · hosts · default ApplicationSiteCatalog revision — content id generation — update order atomic Redis script INCR generation SET snapshot PUBLISH event proc subscribers NO REDIS READ · NO DESCRIPTOR PARSE · NO APP SCAN ON A SITE REQUEST event loads snapshot once 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 Revision identifies routing state; generation orders updates, so delayed events cannot roll a worker back. Invalid updates are dropped; the previous valid catalog keeps serving.
Fig. 4 — Configuration changes are distributed once; website requests resolve hot local state.

Each proc subscribes before loading the current snapshot and rejects older generations, so a delayed update cannot roll one worker backward. The catalog also carries the resolved app target, keeping the route and the app version it points to in one coherent generation.

The OpenResty matrix is generated from one platform definition across the maintained local, ECS-reference, and Kubernetes proxy templates. It reserves the configured control-plane prefix and established API, authentication, streaming, socket, and monitoring paths before the site catch-all. Root, clean paths, and /sites/* then enter the site resolver with forwarded host and scheme context.

08 Know which publishing mechanism you need

An application-hosted website and KDCube public content can appear on the same domain, but they own different lifecycle contracts.

Application-hosted website@public_content publication
Complete main-view file treeIndependently published records and pages
HTML, JavaScript, CSS, images, browser routesArticles, metadata, catalogs, and public entries
Alias, host, and optional default selectionPublic slug, content alias, publish/retract state
Multipage and SPA fallbackSitemap, JSON-LD, canonical-page lifecycle
App UI build and storageDurable content registry and hot content tier

Use a site for the browser shell and pages of a complete app experience. Use @public_content for independently indexed records. A site may link to those records without absorbing their publication model.

09 The end-to-end builder check

  1. Build one app ui.main_view with a complete index.html file tree.
  2. Use relative browser asset paths; for Vite, set base: './'.
  3. Add an enabled ui.main_view.site with a unique alias.
  4. Keep the control plane on a non-root proxy.route_prefix.
  5. Refresh the runtime when platform/proxy code changed; use normal app reloads for later app source or descriptor changes.
  6. Verify /sites/{alias}/, one real asset, one directory index, and one SPA route through the real proxy.
  7. Verify matching-host, default, unmatched-host, and disabled-site behavior.
  8. Verify the control plane, APIs, streaming, sockets, /profile, and one protected app operation still retain their owners.
  9. For a public hostname, route it to KDCube and preserve Host and scheme.
  10. Inspect cache headers: entry HTML revalidates; content-hashed assets may be cached as immutable.
CURRENT CLI DETAIL

For several hosts, use a real YAML list in the descriptor. The current kdcube bundle --set-config value parser accepts scalar values; a JSON-looking list passed to that flag becomes one literal string rather than a YAML list.

The result is one app release with one frontend artifact and several deliberate ways to address it. The site can stay local by alias, share an origin with the control plane, sit beside another root website, or receive its own domain. None of those choices splits the website away from the APIs, agents, tools, and runtime configuration it was built to present.

10 Related articles and documentation

KDCube Engineering
12.07.2026 · Deep