Skip to content

Add base path support#5712

Draft
tianwei-liu wants to merge 4 commits intomainfrom
base-path-support
Draft

Add base path support#5712
tianwei-liu wants to merge 4 commits intomainfrom
base-path-support

Conversation

@tianwei-liu
Copy link
Copy Markdown
Contributor

@tianwei-liu tianwei-liu commented Feb 5, 2026

Summary

Add support for deploying Flowise behind a reverse proxy or ingress with a custom base path (e.g., /flowise). This allows serving Flowise from a sub-directory URL like https://example.com/flowise/.

Fixes: #2911

Problem

When deploying Flowise behind a reverse proxy with a path prefix:

  • Static assets (JS, CSS, images) fail to load because paths are relative to root
  • API calls go to wrong endpoints
  • Client-side navigation breaks
  • Authentication redirects go to wrong URLs

Solution

Server-side (FLOWISE_BASE_PATH)

  • Mount API routes at {basePath}/api/v1/
  • Serve static UI files at {basePath}/
  • Update authentication redirects to include base path
  • Add SPA fallback for client-side routing under base path
  • Redirect root / to base path when configured

UI-side (VITE_BASE_PATH)

  • Configure Vite base option for correct asset URL prefixes
  • Set React Router basename for client-side navigation
  • Update API baseURL to include base path
  • Fix relative paths in index.html (manifest.json, favicon, etc.)

Configuration

# Server
FLOWISE_BASE_PATH=/flowise

# UI (build time)
VITE_BASE_PATH=/flowise

Files Changed

Server

  • packages/server/src/index.ts - Base path routing, static serving, SPA fallback
  • packages/server/src/enterprise/middleware/passport/index.ts - Auth redirects
  • packages/server/src/enterprise/controllers/account.controller.ts - Logout redirect
  • packages/server/.env.example - Add FLOWISE_BASE_PATH example

UI

  • packages/ui/vite.config.js - Vite base option
  • packages/ui/src/config.js - React Router basename
  • packages/ui/src/store/constant.js - API baseURL
  • packages/ui/src/index.jsx - Pass basename to BrowserRouter
  • packages/ui/index.html - Fix relative asset paths
  • packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavItem/index.jsx - Fix double basename
  • packages/ui/src/store/context/ErrorContext.jsx - Fix path comparison
  • packages/ui/src/views/auth/signIn.jsx - Fix SSO redirect
  • packages/ui/src/views/auth/register.jsx - Fix SSO redirect
  • packages/ui/src/views/organization/index.jsx - Fix SSO redirect
  • packages/ui/src/views/account/index.jsx - Fix login redirect
  • packages/ui/.env.example - Add VITE_BASE_PATH example

Testing

  1. Build UI: VITE_BASE_PATH=/flowise pnpm build
  2. Run server: FLOWISE_BASE_PATH=/flowise pnpm start
  3. Access: http://localhost:3000/flowise/
  4. Verify:
    • Static assets load correctly
    • Client-side navigation works
    • API calls work
    • Login/logout redirects work
    • Direct URL access works (e.g., /flowise/chatflows)

Review Feedback Addressed

  • HIGH: Escape special regex characters in basePath before using in RegExp
  • MEDIUM: Move basePath to class property to avoid duplicate declarations
  • ℹ️ Rate limiting warnings are pre-existing issues, not introduced by this PR

tianwei-liu and others added 2 commits February 4, 2026 18:20
Enable Flowise to run behind an ingress with a path prefix (e.g., /flowise)
so it can be accessed via URLs like https://example.com/flowise/.

- Add VITE_BASE_PATH env var for UI build (vite base option + React Router basename)
- Add FLOWISE_BASE_PATH env var for server (API routes, static files, admin dashboard)
- Update baseURL in constant.js to include base path for API calls

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @tianwei-liu, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces comprehensive support for deploying the application under a custom base path, which is crucial for environments where the application is served from a sub-directory or behind a reverse proxy. It involves updating both the backend API endpoints and the frontend UI routing and asset loading mechanisms to correctly resolve paths relative to the specified base path. Additionally, a minor update to the file system utility functions was made.

Highlights

  • Base Path Configuration: Introduced FLOWISE_BASE_PATH and VITE_BASE_PATH environment variables for server and UI respectively, enabling the application to be deployed under a custom subpath.
  • Server-Side Routing Updates: Modified Express.js routes, API endpoints, and redirect URLs in the backend to correctly incorporate the configured base path, ensuring proper routing when served from a sub-directory.
  • UI-Side Routing Updates: Configured React Router and Vite to utilize the VITE_BASE_PATH for correct asset loading and navigation within the user interface when deployed to a subpath.
  • File System API Modernization: Updated fs.promises.rmdir to the more modern and versatile fs.promises.rm in storageUtils.ts for recursive directory deletion.
Changelog
  • packages/components/src/storageUtils.ts
    • Replaced deprecated fs.promises.rmdir with fs.promises.rm for recursive directory deletion.
  • packages/server/.env.example
    • Added a commented-out FLOWISE_BASE_PATH variable to the example environment file for base path configuration.
  • packages/server/src/enterprise/middleware/passport/index.ts
    • Implemented FLOWISE_BASE_PATH to prefix authentication API routes (/api/v1/auth/resolve, /api/v1/auth/refreshToken, /api/v1/auth/login) and associated redirect URLs, ensuring base path compatibility for authentication flows.
  • packages/server/src/index.ts
    • Integrated FLOWISE_BASE_PATH into the BullMQ dashboard base path.
    • Updated URL matching regular expressions and API key blacklist URLs to account for the base path.
    • Applied FLOWISE_BASE_PATH to the main API router and the /api/v1/ip endpoint.
    • Modified static file serving and SPA fallback logic to correctly handle the base path, including a root redirect to the base path if configured.
  • packages/ui/.env.example
    • Added a commented-out VITE_BASE_PATH variable to the example environment file for UI base path configuration.
  • packages/ui/src/config.js
    • Configured React Router's basename using the VITE_BASE_PATH environment variable, allowing the UI to be served from a subpath.
  • packages/ui/src/index.jsx
    • Passed the dynamically configured basename to BrowserRouter for correct routing.
  • packages/ui/src/routes/index.jsx
    • Removed redundant basename parameter from useRoutes as it's now handled by BrowserRouter.
  • packages/ui/src/store/constant.js
    • Adjusted the baseURL constant to incorporate VITE_BASE_PATH when constructing the API base URL, ensuring correct API calls from the UI.
  • packages/ui/vite.config.js
    • Set Vite's base configuration option using process.env.VITE_BASE_PATH for correct asset paths during UI builds.
    • Updated dotenv.config to explicitly specify the .env file path for consistent environment variable loading.
Activity
  • No specific activity (comments, reviews, etc.) was provided in the context for this pull request.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Comment thread packages/server/src/index.ts Fixed
Comment thread packages/server/src/index.ts Fixed
Comment thread packages/server/src/index.ts Fixed
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for a custom base path, allowing the application to be hosted under a sub-directory. The changes span across the server and UI packages, updating route definitions, API URLs, and static file serving to respect the FLOWISE_BASE_PATH and VITE_BASE_PATH environment variables.

My review focuses on the implementation details. I've identified a couple of areas for improvement in packages/server/src/index.ts:

  • A potential bug where an unescaped basePath is used in a regular expression, which could lead to incorrect URL matching if the path contains special characters.
  • A maintainability issue with the basePath variable being defined in multiple places.

Overall, the changes are good and address the feature requirement. Addressing the feedback will make the implementation more robust and maintainable.

Comment thread packages/server/src/index.ts Outdated
Comment on lines +213 to +214
const URL_CASE_INSENSITIVE_REGEX: RegExp = new RegExp(`${basePath}/api/v1/`, 'i')
const URL_CASE_SENSITIVE_REGEX: RegExp = new RegExp(`${basePath}/api/v1/`)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When constructing a regular expression from a variable, it's a good practice to escape any special regex characters within that variable to prevent unexpected behavior. The basePath could potentially contain characters that have special meaning in regex, like a dot (.), which could lead to incorrect URL matching. I suggest escaping basePath before using it in the RegExp constructor.

Suggested change
const URL_CASE_INSENSITIVE_REGEX: RegExp = new RegExp(`${basePath}/api/v1/`, 'i')
const URL_CASE_SENSITIVE_REGEX: RegExp = new RegExp(`${basePath}/api/v1/`)
const URL_CASE_INSENSITIVE_REGEX: RegExp = new RegExp(`${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/api/v1/`, 'i')
const URL_CASE_SENSITIVE_REGEX: RegExp = new RegExp(`${basePath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/api/v1/`)

Comment thread packages/server/src/index.ts Outdated
if (next) next()
})

const basePath = process.env.FLOWISE_BASE_PATH || ''
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The basePath constant is defined here and also within the initDatabase method. To avoid redundancy and improve maintainability, consider defining basePath as a class property of the App class. This would provide a single source of truth for the base path throughout the class.

- Move basePath to class property for single source of truth
- Escape special regex characters in basePath to prevent incorrect URL matching
- Use this.basePath consistently throughout the class

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

const rateLimiter = this.rateLimiterManager.getRateLimiterById(id)
this.app.use('/admin/queues', rateLimiter, verifyTokenForBullMQDashboard, this.queueManager.getBullBoardRouter())
this.app.use(`${this.basePath}/admin/queues`, rateLimiter, verifyTokenForBullMQDashboard, this.queueManager.getBullBoardRouter())

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

Copilot Autofix

AI 2 months ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

Comment on lines +360 to +362
this.app.get(`${this.basePath}/*`, (req: Request, res: Response) => {
res.sendFile(uiHtmlPath)
})

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 2 months ago

In general, the problem is fixed by adding rate limiting middleware in front of the route handler that performs filesystem access (res.sendFile(uiHtmlPath)), so that any client can only trigger this expensive operation at a controlled rate. Since this project already has a RateLimiterManager utility and uses it for the BullMQ admin dashboard, the best fix is to reuse that infrastructure: create a specific rate limiter for SPA UI routes and apply it to the fallback GET handlers that call sendFile.

Concretely, inside packages/server/src/index.ts within the config (or equivalent) method where the SPA UI routes are set up, we will:

  • Initialize a new rate limiter via this.rateLimiterManager.addRateLimiter, giving it an ID such as 'ui_spa_route'. We will choose reasonable defaults (e.g., 60-second window, 100 requests), and allow a custom message via an environment variable while not changing other behavior.
  • Retrieve the corresponding middleware via this.rateLimiterManager.getRateLimiterById.
  • Apply this middleware to:
    • The basePath SPA fallback route: this.app.get(${this.basePath}/*, ...).
    • The root SPA fallback this.app.get('*', ...) when this.basePath is not set.
      We will not alter the static file middleware (express.static(uiBuildPath)) or the redirect from '/' to this.basePath; only the sendFile(uiHtmlPath) routes need the limiter. All changes remain within packages/server/src/index.ts, relying only on the existing RateLimiterManager import.
Suggested changeset 1
packages/server/src/index.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -350,6 +350,16 @@
 
         this.app.use(this.basePath || '/', express.static(uiBuildPath))
 
+        // Initialize UI SPA rate limiter for routes serving index.html
+        const uiRateLimiterId = 'ui_spa_route'
+        await this.rateLimiterManager.addRateLimiter(
+            uiRateLimiterId,
+            60,
+            100,
+            process.env.UI_RATE_LIMIT_MESSAGE || 'Too many requests to UI, please try again later.'
+        )
+        const uiRateLimiter = this.rateLimiterManager.getRateLimiterById(uiRateLimiterId)
+
         // Redirect root to basePath if basePath is set
         if (this.basePath) {
             this.app.get('/', (req: Request, res: Response) => {
@@ -357,13 +367,17 @@
             })
 
             // SPA fallback for routes under basePath only
-            this.app.get(`${this.basePath}/*`, (req: Request, res: Response) => {
-                res.sendFile(uiHtmlPath)
-            })
+            this.app.get(
+                `${this.basePath}/*`,
+                uiRateLimiter,
+                (req: Request, res: Response) => {
+                    res.sendFile(uiHtmlPath)
+                }
+            )
         } else {
             // SPA fallback for all routes when no basePath
             // Only serve index.html for routes that don't look like file requests
-            this.app.get('*', (req: Request, res: Response, next) => {
+            this.app.get('*', uiRateLimiter, (req: Request, res: Response, next) => {
                 // Skip if it looks like a file request (has extension)
                 const ext = path.extname(req.path)
                 if (ext) {
EOF
@@ -350,6 +350,16 @@

this.app.use(this.basePath || '/', express.static(uiBuildPath))

// Initialize UI SPA rate limiter for routes serving index.html
const uiRateLimiterId = 'ui_spa_route'
await this.rateLimiterManager.addRateLimiter(
uiRateLimiterId,
60,
100,
process.env.UI_RATE_LIMIT_MESSAGE || 'Too many requests to UI, please try again later.'
)
const uiRateLimiter = this.rateLimiterManager.getRateLimiterById(uiRateLimiterId)

// Redirect root to basePath if basePath is set
if (this.basePath) {
this.app.get('/', (req: Request, res: Response) => {
@@ -357,13 +367,17 @@
})

// SPA fallback for routes under basePath only
this.app.get(`${this.basePath}/*`, (req: Request, res: Response) => {
res.sendFile(uiHtmlPath)
})
this.app.get(
`${this.basePath}/*`,
uiRateLimiter,
(req: Request, res: Response) => {
res.sendFile(uiHtmlPath)
}
)
} else {
// SPA fallback for all routes when no basePath
// Only serve index.html for routes that don't look like file requests
this.app.get('*', (req: Request, res: Response, next) => {
this.app.get('*', uiRateLimiter, (req: Request, res: Response, next) => {
// Skip if it looks like a file request (has extension)
const ext = path.extname(req.path)
if (ext) {
Copilot is powered by AI and may make mistakes. Always verify output.
@tianwei-liu
Copy link
Copy Markdown
Contributor Author

Addressed review feedback in commit 5b16a82:

  1. Regex escaping (HIGH): Added escapedBasePath that escapes special regex characters before constructing RegExp patterns
  2. Class property (MEDIUM): Moved basePath to a class property initialized in constructor, eliminating duplicate declarations

The rate limiting warnings from the security bot are pre-existing issues not introduced by this PR - the SPA fallback routes existed before, and the BullMQ dashboard route already has rate limiting middleware applied.

- Use absolute paths in index.html for manifest.json, favicon.ico,
  logo192.png so Vite can transform them with base path
- Remove manual config.basename prepending in NavItem since
  BrowserRouter already handles it (was causing doubled paths)
- Add basePath to server logout redirects in account.controller.ts
- Fix SSO login redirects to include basePath in signIn, register,
  and organization pages
- Fix ErrorContext path comparison to handle base path
- Use navigate() instead of window.location.href in account page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comment on lines +366 to +375
this.app.get('*', (req: Request, res: Response, next) => {
// Skip if it looks like a file request (has extension)
const ext = path.extname(req.path)
if (ext) {
// It's a file request, let it 404 naturally
return next()
}
// Otherwise serve the SPA
res.sendFile(uiHtmlPath)
})

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
a file system access
, but is not rate-limited.

Copilot Autofix

AI 2 months ago

In general, the fix is to apply a rate-limiting middleware to the route(s) that trigger filesystem access so that a single client cannot make unlimited requests in a short time. This can be done with either an existing project-specific rate limiter (RateLimiterManager) or a standard package such as express-rate-limit. Since RateLimiterManager is already imported from ./utils/rateLimit, the best fix with minimal functional change is to instantiate an appropriate limiter (e.g., per-IP, reasonable max requests per window) and apply it only to the SPA fallback and optionally the static UI route, without altering the route logic itself.

Concretely for packages/server/src/index.ts inside async config() where the UI static serving is configured, we will: (1) create a rate limiter instance using RateLimiterManager (e.g., const uiRateLimiter = RateLimiterManager.getLimiter('ui-static') or similar; since we don't see its API, we’ll define a plausible, minimal interface consistent with a typical Express middleware factory, such as RateLimiterManager.getRateLimiter(options) that returns an Express middleware function); (2) apply this rate limiter to express.static(uiBuildPath) via this.app.use(...) so that all static UI asset requests are limited; and (3) also apply the same limiter to the SPA fallback route(s) (this.app.get('*', ...) and ${this.basePath}/*) using this.app.get(path, uiRateLimiter, handler) so file-serving is rate-limited. This keeps behavior identical under normal load while protecting against abusive request rates.

Because we must not change imports, and RateLimiterManager is already imported, the only needed changes are within the shown config() section: define a limiter middleware variable before the UI routes and insert it into the relevant app.use/app.get calls. No new methods or external dependencies are required.

Suggested changeset 1
packages/server/src/index.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts
--- a/packages/server/src/index.ts
+++ b/packages/server/src/index.ts
@@ -348,8 +348,15 @@
         const uiBuildPath = path.join(packagePath, 'build')
         const uiHtmlPath = path.join(packagePath, 'build', 'index.html')
 
-        this.app.use(this.basePath || '/', express.static(uiBuildPath))
+        // Apply rate limiting to UI static assets and SPA fallback to prevent abuse
+        const uiRateLimiter = RateLimiterManager.getRateLimiter({
+            keyPrefix: 'ui-static',
+            windowMs: 15 * 60 * 1000,
+            max: 1000
+        })
 
+        this.app.use(this.basePath || '/', uiRateLimiter, express.static(uiBuildPath))
+
         // Redirect root to basePath if basePath is set
         if (this.basePath) {
             this.app.get('/', (req: Request, res: Response) => {
@@ -357,13 +363,13 @@
             })
 
             // SPA fallback for routes under basePath only
-            this.app.get(`${this.basePath}/*`, (req: Request, res: Response) => {
+            this.app.get(`${this.basePath}/*`, uiRateLimiter, (req: Request, res: Response) => {
                 res.sendFile(uiHtmlPath)
             })
         } else {
             // SPA fallback for all routes when no basePath
             // Only serve index.html for routes that don't look like file requests
-            this.app.get('*', (req: Request, res: Response, next) => {
+            this.app.get('*', uiRateLimiter, (req: Request, res: Response, next) => {
                 // Skip if it looks like a file request (has extension)
                 const ext = path.extname(req.path)
                 if (ext) {
EOF
@@ -348,8 +348,15 @@
const uiBuildPath = path.join(packagePath, 'build')
const uiHtmlPath = path.join(packagePath, 'build', 'index.html')

this.app.use(this.basePath || '/', express.static(uiBuildPath))
// Apply rate limiting to UI static assets and SPA fallback to prevent abuse
const uiRateLimiter = RateLimiterManager.getRateLimiter({
keyPrefix: 'ui-static',
windowMs: 15 * 60 * 1000,
max: 1000
})

this.app.use(this.basePath || '/', uiRateLimiter, express.static(uiBuildPath))

// Redirect root to basePath if basePath is set
if (this.basePath) {
this.app.get('/', (req: Request, res: Response) => {
@@ -357,13 +363,13 @@
})

// SPA fallback for routes under basePath only
this.app.get(`${this.basePath}/*`, (req: Request, res: Response) => {
this.app.get(`${this.basePath}/*`, uiRateLimiter, (req: Request, res: Response) => {
res.sendFile(uiHtmlPath)
})
} else {
// SPA fallback for all routes when no basePath
// Only serve index.html for routes that don't look like file requests
this.app.get('*', (req: Request, res: Response, next) => {
this.app.get('*', uiRateLimiter, (req: Request, res: Response, next) => {
// Skip if it looks like a file request (has extension)
const ext = path.extname(req.path)
if (ext) {
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants