Routing Modes — Subpath vs Subdomain
Choose whether Scrydon's browser-facing apps share one hostname or use one hostname per app.
Scrydon's browser-facing apps support two deployment-time routing modes. The same application
images serve both modes; routing.mode selects the public URLs, base paths, Gateway API
objects, cookies, and CORS configuration.
Subpath (subpath, default) | Subdomain (subdomain) | |
|---|---|---|
| Example URLs | app.example.com/cortexapp.example.com/analytics | cortex.example.comanalytics.example.com |
| DNS | One A record | One wildcard or one A record per app |
| TLS | One host certificate | One wildcard or one certificate per app |
| Gateway | One gateway-frontdoor with an http and an https listener | The same single gateway-frontdoor, with one HTTPS listener per enabled app hostname |
| Routes | One HTTPRoute per enabled app, carrying its routing.paths.* prefixes | One HTTPRoute per enabled browser/realtime app, matching / on its own hostname |
| Cookies | Host-scoped | Shared registrable domain |
| CORS | One public origin | One public origin per app |
| Best fit | Customer and on-premises installs | SaaS or independent app domains |
Use the default subpath mode unless you need separate DNS, TLS, or SSO policy per app.
How base paths work
The chart resolves each application's prefix and injects BASE_PATH. The app mounts routes and
assets below that prefix, injects the value into client code, and configures TanStack Router
with the same base path. API services follow the same rule.
The Tables service (apiTable) is the one exception. It has no public route in subdomain mode;
other services reach it only over Dapr, at /api/table. So it mounts at routing.paths.apiTable
in both modes. Keep that value at its default /api/table, because internal callers address it
there.
Edge-only strip-prefix rewriting is insufficient because server-rendered pages and hashed assets need an application-aware public base path. No rebuild is required when changing modes.
If you put your own proxy or CDN in front of the chart's front door, send each app's whole
prefix to it, including <base>/i18n/*. That path serves the interface translations as a script
cached for a year. A proxy that routes the pages but not that path still works, but every browser
then reloads once and receives the translations inside each page instead of from its cache.
Subpath quick start
routing:
mode: subpath
host: app.example.com
# paths:
# cortex: /cortex
# agentic: /agentic
# analytics: /analytics
# platform: /platform
# apiAuth: /api/auth
# apiOntology: /api/ontology
# apiTable: /api/table
# agenticRealtime: /agentic/realtimeCreate one A record for app.example.com. With TLS enabled, the https listener references
tls-frontdoor, and the chart renders one HTTPRoute per enabled app carrying that app's
paths. Rule order is irrelevant: the Gateway API mandates longest-matching-prefix precedence
across every route on a listener, so /agentic/realtime always outranks /agentic and the
apex / rule is simply the lowest-precedence one.
The front door also routes /api/router and /api/mcp to the platform whenever auth is
enabled — the AI Gateway and Scrydon MCP endpoints developers point Claude Code, Codex, and
Cursor at. They are deliberately not routing.paths keys: unlike apiAuth, whose value
also becomes the server's own mount point, these two are fixed on both ends (the CLI writes
them into ANTHROPIC_BASE_URL and the Claude Code and Codex MCP entries, and the AI Gateway
settings page into every app it connects), so a configurable value could only
route somewhere neither end speaks. In subdomain mode they need no rule of their own — the
auth host already sends its whole prefix to the platform. See
AI Gateway.
/api/collaboration is routed the same way and for the same reason: Cortex chat calls the
platform there, from paths fixed in its own code.
Subdomain quick start
routing:
mode: subdomain
auth:
publicUrl: https://auth.example.com
gateway: { hostname: auth.example.com }
corsOrigins:
- https://app.example.com
- https://cortex.example.com
- https://agentic.example.com
- https://ws-agentic.example.com
- https://analytics.example.com
platform:
publicUrl: https://app.example.com
gateway: { hostname: app.example.com }
cortex:
publicUrl: https://cortex.example.com
gateway: { hostname: cortex.example.com }
agentic:
publicUrl: https://agentic.example.com
publicSocketUrl: https://ws-agentic.example.com
app:
gateway: { hostname: agentic.example.com }
realtime:
gateway: { hostname: ws-agentic.example.com }
analytics:
publicUrl: https://analytics.example.com
gateway: { hostname: analytics.example.com }Create an A record per host, or use a wildcard. Each enabled app gets its own HTTPS listener
on the single Gateway, referencing its corresponding TLS Secret (tls-auth, tls-platform,
tls-cortex, tls-agentic, tls-agentic-realtime, tls-analytics) unless
gateway.tls.existingSecret points every listener at one certificate. Cross-subdomain
sessions require every public URL to share the same registrable domain.
In subdomain mode, browsers sign in against auth.publicUrl, which is a different host from
every app. Each app's Content-Security-Policy allows that origin in connect-src, taken from the
same auth.publicUrl value. Set it to the exact URL browsers reach. On earlier releases, the
platform, Cortex, Horizon and Analytics policies did not include the auth host. On a domain other
than scrydon.com, the browser refused sign-in with a Content Security Policy error.
The realtime sticky-cookie annotations that used to sit under agentic.realtime are gone.
Session affinity for Gateway API backends is not part of the standard channel, so
agentic-realtime has no affinity if you run it with more than one replica in subdomain
mode. It ships replicas: 1.
Marimo notebooks
Marimo does not have a public host, path, HTTPRoute, or TLS secret. The notebook document, Connect request, cell Run request, HTTP proxying, and WebSocket proxying all stay on the authenticated Analytics origin:
- subpath:
https://app.example.com/analytics/notebooks/... - subdomain:
https://analytics.example.com/notebooks/...
The stateless document renderer and isolated runtimes are private cluster services. Do not
create a marimo.example.com DNS record or expose runtime ports directly.
DNS and TLS
For issuer choices, private domains, and corporate certificates, see TLS Certificates.
Subpath requires only:
app.example.com A <gateway load-balancer IP>Subdomain commonly requires:
app.example.com A <gateway load-balancer IP>
cortex.example.com A <gateway load-balancer IP>
agentic.example.com A <gateway load-balancer IP>
ws-agentic.example.com A <gateway load-balancer IP>
analytics.example.com A <gateway load-balancer IP>
auth.example.com A <gateway load-balancer IP>The IP is the one published by your Gateway API implementation's LoadBalancer Service —
the chart never provisions a public IP and every Scrydon Service stays ClusterIP.
Switching a live deployment
- Provision the target DNS records and certificates.
- Change
routing.modeand the matching host/public URL values. - Upgrade the same chart version with your values file.
- Verify the Gateway listeners, HTTPRoutes, certificates, app health, sign-in, and WebSocket routes.
- Cut over DNS. Users may need to sign in again because cookie scope changed.
helm upgrade scrydon oci://scrydonops.azurecr.io/scrydon/charts/scrydon \
--version "${CURRENT_VERSION}" \
--namespace scrydon-platform \
-f values.customer.yaml \
--wait
# One Gateway in both modes — check its listeners went Programmed
kubectl get gateway -n scrydon-platform
kubectl describe gateway gateway-frontdoor -n scrydon-platform
# Subpath: one HTTPRoute per enabled app, carrying its path prefixes
# Subdomain: one HTTPRoute per enabled app, matching / on its own hostname
kubectl get httproute -AChanging the mode back and upgrading performs the routing rollback. It does not affect notebook runtime isolation because Marimo remains behind Analytics in both modes.
Multiple namespaces
One Gateway serves every namespace, in both routing modes. An HTTPRoute attaches to a
Gateway in another namespace as a first-class operation, so splitting services across
namespaces via namespaces.* renders no extra front doors: gateway-frontdoor stays in
scrydon-platform, and each app's HTTPRoute lives beside its own backend Service and points
back at that one Gateway.
The Gateway admits routes from exactly the namespaces the chart is rendering into — its
allowedRoutes selector lists them by name, so re-run helm upgrade after any namespaces.*
change and the selector moves with the routes.
TLS is one place too. Every listener references a Secret in the Gateway's own namespace, so
tls-frontdoor (or gateway.tls.existingSecret) is created only in scrydon-platform,
whatever namespaces.agentic / namespaces.analytics / namespaces.cortex are set to. The
per-namespace certificate copies and the Traefik SNI-reuse workaround that the Ingress edge
needed are gone.