feat: add configurable BASE_PATH for subpath deployments#687
feat: add configurable BASE_PATH for subpath deployments#687rjmayott wants to merge 6 commits intositeboon:mainfrom
Conversation
Enable CloudCLI to be served from a subpath (e.g. /s/myproject/) instead of only root (/). Useful for running multiple instances behind a single reverse proxy. When BASE_PATH is not set, behavior is identical to current. Changes: - New src/utils/basePath.ts helper (BASE_PATH constant + assetUrl) - authenticatedFetch auto-prepends BASE_PATH for root-relative URLs - Bare fetch() calls (auth endpoints, health) manually prefixed - WebSocket URLs (/ws, /shell) include BASE_PATH - Component image src attributes use assetUrl() - EventSource and hardcoded href paths prefixed - server/index.js: read BASE_PATH env, inject window globals into index.html, serve manifest.json dynamically with prefixed paths, strip BASE_PATH in WebSocket routing, disable express.static index serving so SPA catch-all handles injection - public/sw.js: derive base path from SW location at runtime - index.html + main.jsx: SW registration uses injected global - vite.config.js: base option from VITE_BASE_PATH env - README: document BASE_PATH usage with Caddy example Key design decisions: - Runtime injection (window.__CLOUDCLI_BASE_PATH__) rather than build-time, so one build works at any subpath - VITE_BASE_PATH=./ at build time for relative asset paths - BASE_PATH env on server for runtime path injection
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds subpath deployment support: build-time Vite base, runtime base-path injection into served HTML, server-side URL rewriting and manifest rewriting, service-worker and client registration updated to respect base path, and client helpers to resolve API, WebSocket, SSE, and asset URLs via the configured base path. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Server
participant SW as ServiceWorker
participant API as API/WebSocket
Server->>Browser: Serve HTML with window.__CLOUDCLI_BASE_PATH__
Browser->>Browser: Read BASE_PATH from window
Browser->>SW: Register Service Worker at ${BASE_PATH}/sw.js
activate SW
SW->>SW: Derive BASE_PATH from its script pathname
deactivate SW
Browser->>API: Request ${BASE_PATH}/api/... or connect to ${BASE_PATH}/ws
activate API
Server->>Server: Strip BASE_PATH for internal routing and manifest rewriting
API-->>Browser: Response / WebSocket handshake
deactivate API
Browser->>SW: Fetch ${BASE_PATH}/assets/...
activate SW
SW->>SW: Match/cache requests under ${BASE_PATH}/assets or manifest
SW-->>Browser: Serve cached or network asset
deactivate SW
Possibly Related PRs
Suggested Reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
public/sw.js (1)
27-48:⚠️ Potential issue | 🟡 MinorUse parsed path checks instead of substring matching in the service worker.
event.request.url.includes(...)can match query strings or unrelated/cross-origin URLs. Parse the URL and compareorigin/pathnameso only this app’s scoped routes are skipped or cached.🛡️ Proposed route matching cleanup
self.addEventListener('fetch', event => { - const url = event.request.url; + const requestUrl = new URL(event.request.url); + + if (requestUrl.origin !== self.location.origin) { + return; + } + + const pathname = requestUrl.pathname; // Never intercept API requests or WebSocket upgrades - if (url.includes(`${BASE_PATH}/api/`) || url.includes(`${BASE_PATH}/ws`)) { + if ( + pathname.startsWith(`${BASE_PATH}/api/`) || + pathname === `${BASE_PATH}/ws` || + pathname.startsWith(`${BASE_PATH}/ws/`) + ) { return; } @@ // Hashed assets (JS/CSS in /assets/) — cache-first since filenames change per build - if (url.includes(`${BASE_PATH}/assets/`)) { + if (pathname.startsWith(`${BASE_PATH}/assets/`)) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@public/sw.js` around lines 27 - 48, The service worker currently uses substring matching on event.request.url which can produce false positives; instead parse the request URL with new URL(event.request.url) and compare its origin to self.location.origin (or self.origin) and its pathname to exact scoped routes using startsWith comparisons against BASE_PATH + '/api/' and BASE_PATH + '/ws' (or exact equality for the ws endpoint), and for WebSocket upgrades also check the Upgrade header on event.request; update the early-return logic around event.request.url, the navigation handling (event.request.mode === 'navigate') and the assets branch (url.includes(`${BASE_PATH}/assets/`)) to use the parsed urlObj.pathname and origin checks before calling event.respondWith or returning.src/utils/api.js (1)
104-108:⚠️ Potential issue | 🟠 MajorAvoid putting the auth token in the generated URL for EventSource.
The
searchConversationsUrlfunction is used exclusively withEventSource(insrc/components/sidebar/hooks/useSidebarController.ts:215), which passes the full URL—including thetokenquery parameter—through browser history, proxy logs, analytics, andRefererheaders. Replace the stored auth token with a short-lived scoped token that cannot be misused if leaked, sinceEventSourcedoesn't support custom headers likeauthenticatedFetch.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/api.js` around lines 104 - 108, The current searchConversationsUrl helper (used by EventSource) exposes the persistent auth token in the URL; change it so it does NOT embed the stored auth-token. Update searchConversationsUrl to only build the base URL/params (q, limit) and remove the token logic, then implement a short-lived, scoped token flow: create a server endpoint (e.g., POST /api/sse-token) that mints a one-time/short-lived token tied to the current session and allowed scopes, call that endpoint from the client (from the code that constructs the EventSource in useSidebarController) to fetch the ephemeral token, and append that ephemeral token to the returned URL before constructing EventSource; alternatively switch server to accept the session cookie for SSE so no token is put in the URL. Ensure references: searchConversationsUrl and the EventSource constructor usage in useSidebarController are updated accordingly.
🧹 Nitpick comments (1)
src/utils/basePath.ts (1)
14-16: NormalizeassetUrlinputs in the shared helper.Current call sites pass leading-slash paths, but this helper is now the shared API; central normalization prevents future callers from producing
/baseicons/foo.svgor/base//icons/foo.svg.♻️ Proposed hardening
-export const BASE_PATH = window.__CLOUDCLI_BASE_PATH__ || ''; +const normalizeBasePath = (value?: string): string => { + if (!value || value === '/') { + return ''; + } + + return `/${value}`.replace(/\/+/g, '/').replace(/\/$/, ''); +}; + +export const BASE_PATH = normalizeBasePath(window.__CLOUDCLI_BASE_PATH__); -export const assetUrl = (path: string): string => `${BASE_PATH}${path}`; +export const assetUrl = (path: string): string => { + const normalizedPath = path.startsWith('/') ? path : `/${path}`; + return `${BASE_PATH}${normalizedPath}`; +};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/basePath.ts` around lines 14 - 16, assetUrl currently concatenates BASE_PATH and path verbatim which yields duplicate or missing slashes; update assetUrl to normalize both parts: if BASE_PATH is falsy return path unchanged to preserve existing behavior, otherwise remove any trailing slash from BASE_PATH and any leading slashes from path and join them with a single '/' (referencing BASE_PATH and assetUrl to locate the change).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@README.md`:
- Around line 123-130: The fenced code block in README.md containing the
Caddyfile example is missing a language identifier; update the opening triple
backticks for that snippet to include a language specifier (use "caddy" or
"caddyfile") so markdown linters and syntax highlighters recognize the block
(the block that begins with the lines showing ":443 {", "handle /s/myproject/*
{", and "uri strip_prefix /s/myproject"); simply add the specifier immediately
after the opening ``` and save the file.
In `@server/index.js`:
- Around line 1371-1374: The code currently strips BASE_PATH from pathname using
startsWith, which incorrectly rewrites paths like "/apple/ws"; change the
condition so BASE_PATH is removed only when pathname equals BASE_PATH or when it
is followed by a path-separator. Specifically, update the block that checks
BASE_PATH and pathname (the variables and expressions
pathname.startsWith(BASE_PATH) and pathname.slice(BASE_PATH.length)) to only
slice when pathname === BASE_PATH or pathname.startsWith(BASE_PATH + '/'),
otherwise leave pathname unchanged.
- Around line 2218-2225: The code injects BASE_PATH directly into an inline
script (see BASE_PATH, injection, html.replace) which allows unsafe characters
to break out of the script; fix by safely serializing and escaping the value
before embedding: replace direct string interpolation with a serialized version
(e.g., JSON.stringify or an equivalent serializer) and additionally escape any
'<' characters in the serialized output before constructing injection, then use
that escaped string in the injection variable so html.replace inserts a safe,
quoted JS value.
- Around line 16-17: The app currently defines BASE_PATH but still mounts routes
at root; update route mounting to serve API/static/manifest/sw under the subpath
by prefixing route mounts with BASE_PATH (e.g., use app.use(path.join(BASE_PATH,
'/api'), apiRouter), app.use(path.join(BASE_PATH, '/assets'),
express.static(...)), and app.get(path.join(BASE_PATH, '/manifest.json'), ...)
and app.get(path.join(BASE_PATH, '/sw.js'), ...)). Alternatively add an early
middleware that rewrites incoming requests that start with BASE_PATH by
stripping the prefix and passing the modified url to the existing handlers;
ensure BASE_PATH is normalized (no trailing slash) and used wherever
app.use/app.get or static middleware are registered so direct subpath
deployments route correctly.
In `@src/utils/api.js`:
- Around line 5-7: The code in authenticatedFetch currently calls
rawUrl.startsWith unconditionally which will throw for non-string inputs
(Request, URL objects); change the URL construction to first check typeof rawUrl
=== 'string' and only then use startsWith to prepend BASE_PATH (e.g. const url =
(typeof rawUrl === 'string' && rawUrl.startsWith('/')) ? `${BASE_PATH}${rawUrl}`
: rawUrl), so non-string inputs are returned/used untouched and the function
safely handles Request/URL objects.
---
Outside diff comments:
In `@public/sw.js`:
- Around line 27-48: The service worker currently uses substring matching on
event.request.url which can produce false positives; instead parse the request
URL with new URL(event.request.url) and compare its origin to
self.location.origin (or self.origin) and its pathname to exact scoped routes
using startsWith comparisons against BASE_PATH + '/api/' and BASE_PATH + '/ws'
(or exact equality for the ws endpoint), and for WebSocket upgrades also check
the Upgrade header on event.request; update the early-return logic around
event.request.url, the navigation handling (event.request.mode === 'navigate')
and the assets branch (url.includes(`${BASE_PATH}/assets/`)) to use the parsed
urlObj.pathname and origin checks before calling event.respondWith or returning.
In `@src/utils/api.js`:
- Around line 104-108: The current searchConversationsUrl helper (used by
EventSource) exposes the persistent auth token in the URL; change it so it does
NOT embed the stored auth-token. Update searchConversationsUrl to only build the
base URL/params (q, limit) and remove the token logic, then implement a
short-lived, scoped token flow: create a server endpoint (e.g., POST
/api/sse-token) that mints a one-time/short-lived token tied to the current
session and allowed scopes, call that endpoint from the client (from the code
that constructs the EventSource in useSidebarController) to fetch the ephemeral
token, and append that ephemeral token to the returned URL before constructing
EventSource; alternatively switch server to accept the session cookie for SSE so
no token is put in the URL. Ensure references: searchConversationsUrl and the
EventSource constructor usage in useSidebarController are updated accordingly.
---
Nitpick comments:
In `@src/utils/basePath.ts`:
- Around line 14-16: assetUrl currently concatenates BASE_PATH and path verbatim
which yields duplicate or missing slashes; update assetUrl to normalize both
parts: if BASE_PATH is falsy return path unchanged to preserve existing
behavior, otherwise remove any trailing slash from BASE_PATH and any leading
slashes from path and join them with a single '/' (referencing BASE_PATH and
assetUrl to locate the change).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 4741c193-53b7-4661-9765-41038786e5f1
📒 Files selected for processing (18)
README.mdindex.htmlpublic/sw.jsserver/index.jssrc/components/auth/view/SetupForm.tsxsrc/components/llm-logo-provider/ClaudeLogo.tsxsrc/components/llm-logo-provider/CodexLogo.tsxsrc/components/llm-logo-provider/CursorLogo.tsxsrc/components/llm-logo-provider/GeminiLogo.tsxsrc/components/project-creation-wizard/data/workspaceApi.tssrc/components/settings/view/tabs/api-settings/sections/ApiKeysSection.tsxsrc/components/shell/utils/socket.tssrc/contexts/WebSocketContext.tsxsrc/hooks/useVersionCheck.tssrc/main.jsxsrc/utils/api.jssrc/utils/basePath.tsvite.config.js
- Fix greedy BASE_PATH stripping in WS routing (e.g. /apple/ws) - Escape BASE_PATH injection to prevent XSS via script breakout - Type-check rawUrl in authenticatedFetch for Request/URL objects - Add caddyfile language to README code block
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/utils/api.js (1)
40-52: Optional: collapse the dual BASE_PATH patterns.The auth block mixes two styles:
auth.status/login/registerhand-prefix${BASE_PATH}/api/...on rawfetch, whileauth.user/logoutrely onauthenticatedFetch's auto-prefix with a root-relative path. Both work (unauthenticated calls viaauthenticatedFetchjust skip theAuthorizationheader when no token is present), but the inconsistency is easy to get wrong in future edits — e.g., a contributor adding another pre-auth endpoint could forget the${BASE_PATH}prefix and silently break subpath deployments.Consider routing all of them through
authenticatedFetch(or a shared helper) so there's one source of truth for BASE_PATH handling.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/api.js` around lines 40 - 52, The auth methods (status, login, register) mix direct fetch with `${BASE_PATH}` while user/logout use authenticatedFetch, so consolidate them to use authenticatedFetch (or a small wrapper) to centralize BASE_PATH handling; update status, login, and register to call authenticatedFetch('/api/auth/status') and authenticatedFetch('/api/auth/login', { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(...) }) and similarly for register, keeping user and logout unchanged, and ensure authenticatedFetch continues to skip Authorization when no token so unauthenticated flows still work.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/utils/api.js`:
- Around line 40-52: The auth methods (status, login, register) mix direct fetch
with `${BASE_PATH}` while user/logout use authenticatedFetch, so consolidate
them to use authenticatedFetch (or a small wrapper) to centralize BASE_PATH
handling; update status, login, and register to call
authenticatedFetch('/api/auth/status') and authenticatedFetch('/api/auth/login',
{ method: 'POST', headers: {'Content-Type':'application/json'}, body:
JSON.stringify(...) }) and similarly for register, keeping user and logout
unchanged, and ensure authenticatedFetch continues to skip Authorization when no
token so unauthenticated flows still work.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 6852e6f3-f6f4-41c1-9bd1-4ad30a843687
📒 Files selected for processing (3)
README.mdserver/index.jssrc/utils/api.js
✅ Files skipped from review due to trivial changes (1)
- README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- server/index.js
Add early middleware to strip BASE_PATH prefix from incoming HTTP requests. This allows direct subpath deployments without a reverse proxy, while remaining compatible with proxy setups that already strip the prefix.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
server/index.js (1)
343-350: Icon URL prefixing works for current manifest but should handle edge cases.The manifest currently contains only root-relative paths (
/icons/...), soBASE_PATH + icon.srcis safe. However, if the manifest format changes to include relative paths, absolute URLs, or data URLs, this concatenation would fail. Consider defensive URL handling:Proposed safer URL prefixing
if (BASE_PATH) { manifest.start_url = BASE_PATH + '/'; manifest.scope = BASE_PATH + '/'; + const prefixManifestPath = (src) => { + if (!src || /^(?:[a-z][a-z\d+\-.]*:|\/\/)/i.test(src)) { + return src; + } + const normalizedSrc = src.startsWith('/') + ? src + : `/${src.replace(/^\.\//, '')}`; + return `${BASE_PATH}${normalizedSrc}`; + }; if (manifest.icons) { manifest.icons = manifest.icons.map(icon => ({ ...icon, - src: BASE_PATH + icon.src, + src: prefixManifestPath(icon.src), })); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/index.js` around lines 343 - 350, The manifest icon URL concatenation is brittle; update the manifest.icons mapping (where manifest.icons is transformed) to handle edge cases by skipping prefixing for absolute URLs (starting with "http://", "https://", "//") and data URLs ("data:"), properly joining root-relative ("/...") and relative paths by resolving them against BASE_PATH (use a URL constructor or path-join behavior) and ensure manifest.start_url and manifest.scope remain BASE_PATH + '/'; modify the mapping used in the manifest.icons block to detect these cases and only prefix BASE_PATH when appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/index.js`:
- Around line 16-17: The BASE_PATH constant currently strips trailing slashes
but doesn't ensure a leading slash, so a value like "app" won't match "/app"
requests; update the BASE_PATH normalization (the BASE_PATH definition) to trim
surrounding slashes and then, if non-empty, prefix a single leading slash (or
otherwise set it to an empty string) so manifest generation and request matching
use "/app" style paths consistently.
---
Nitpick comments:
In `@server/index.js`:
- Around line 343-350: The manifest icon URL concatenation is brittle; update
the manifest.icons mapping (where manifest.icons is transformed) to handle edge
cases by skipping prefixing for absolute URLs (starting with "http://",
"https://", "//") and data URLs ("data:"), properly joining root-relative
("/...") and relative paths by resolving them against BASE_PATH (use a URL
constructor or path-join behavior) and ensure manifest.start_url and
manifest.scope remain BASE_PATH + '/'; modify the mapping used in the
manifest.icons block to detect these cases and only prefix BASE_PATH when
appropriate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
BASE_PATH=app now correctly becomes /app. Strips leading and trailing slashes then prepends one, so request matching and manifest generation work consistently.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/index.js`:
- Around line 216-219: In the app.use middleware that compares req.url to
BASE_PATH, parse out the pathname (or use Express's req.path) so you compare
only the path portion (not the query string), and when you strip the BASE_PATH
from req.url (in the block around BASE_PATH and
req.url.slice(BASE_PATH.length)), preserve and reattach the original query
string (if any) to the new req.url; update the logic in the middleware that
references req.url/BASE_PATH so it uses the pathname for matching and
reconstructs req.url = slicedPath + (queryString || '/') or similar to keep
queries intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
There was a problem hiding this comment.
♻️ Duplicate comments (1)
server/index.js (1)
217-218:⚠️ Potential issue | 🟡 MinorPreserve the leading slash after stripping a bare
BASE_PATHwith query params.
/app?next=/projectsnow matches, butreq.url.slice(BASE_PATH.length)becomes?next=/projects; Express should receive/?next=/projectsfor the root route plus query. This is the same query-string edge case previously flagged, but the current patch only fixed the match condition.🐛 Proposed fix
- if (req.url === BASE_PATH || req.url.startsWith(`${BASE_PATH}/`) || req.url.startsWith(`${BASE_PATH}?`)) { - req.url = req.url.slice(BASE_PATH.length) || '/'; + if (req.url === BASE_PATH || req.url.startsWith(`${BASE_PATH}/`) || req.url.startsWith(`${BASE_PATH}?`)) { + const strippedUrl = req.url.slice(BASE_PATH.length); + req.url = strippedUrl + ? (strippedUrl.startsWith('/') ? strippedUrl : `/${strippedUrl}`) + : '/'; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@server/index.js` around lines 217 - 218, When stripping BASE_PATH from req.url (the check in the route matching block), ensure the resulting path preserves a leading slash when the original was root with query params: after computing newUrl = req.url.slice(BASE_PATH.length) || '/', if newUrl starts with '?' prepend '/' so Express receives '/?query' rather than '?query'. Update the logic around req.url = req.url.slice(BASE_PATH.length) || '/' to handle the case where the slice begins with '?' and set req.url to `"/" + newUrl` in that case.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@server/index.js`:
- Around line 217-218: When stripping BASE_PATH from req.url (the check in the
route matching block), ensure the resulting path preserves a leading slash when
the original was root with query params: after computing newUrl =
req.url.slice(BASE_PATH.length) || '/', if newUrl starts with '?' prepend '/' so
Express receives '/?query' rather than '?query'. Update the logic around req.url
= req.url.slice(BASE_PATH.length) || '/' to handle the case where the slice
begins with '?' and set req.url to `"/" + newUrl` in that case.
…ponse Flip from blacklist (skip known routes, catch everything) to whitelist (only handle /assets/ and navigation). Routes like /session/<id> were not skipped, causing caches.match() to return undefined on cache miss, which respondWith() rejects as invalid Response. Now only assets get cache-first and navigation gets offline fallback. Everything else passes through to the network untouched.
Summary
/s/myproject/) viaBASE_PATHenvironment variablewindow.__CLOUDCLI_BASE_PATH__) rather than build-time config, so a single build works at any subpathBASE_PATHis not set, behavior is identical to current — zero impact on existing deploymentsDetails
Frontend:
src/utils/basePath.tsexportsBASE_PATHandassetUrl()helperauthenticatedFetchauto-prependsBASE_PATHfor root-relative URLs (covers 50+ callers across 20 files without touching each one)fetch()calls (auth endpoints, health check) andEventSourceURLs manually prefixed/ws,/shell) includeBASE_PATHsrcattributes useassetUrl()basenameuseswindow.__ROUTER_BASENAME__(already existed in App.tsx, just needed the value injected)Server (
server/index.js):BASE_PATHfrom env, strips trailing slashwindow.__CLOUDCLI_BASE_PATH__andwindow.__ROUTER_BASENAME__intoindex.htmlvia<script>tagmanifest.jsondynamically with base-path-prefixedstart_url,scope, and iconsrcpathsBASE_PATHfrom WebSocket pathname before routingindex: falseonexpress.staticso the SPA catch-all handlesindex.html(required for injection)Service Worker (
public/sw.js):self.location.pathnameat runtimeBuild (
vite.config.js):VITE_BASE_PATHenv for Vite'sbaseoption (default/)VITE_BASE_PATH=./for relative asset pathsUsage
Reverse proxy example (Caddy):
Test plan
BASE_PATH— app works athttp://localhost:3001/(regression test passed)BASE_PATH=/s/test, run behind Caddy reverse proxy — full UI loads at/s/test//s/test/wsand/s/test/shell/s/test/api/...npm run build)Summary by CodeRabbit
New Features
Documentation