46864 frontend [ Configuration ] Public forms via URL-path /forms/#217
46864 frontend [ Configuration ] Public forms via URL-path /forms/#217EBirkenfeld wants to merge 7 commits into
Conversation
…' of https://github.com/pneumaticapp/pneumaticworkflow into frontend/configuration/46864__public_forms_via_url_path
…' of https://github.com/pneumaticapp/pneumaticworkflow into frontend/configuration/46864__public_forms_via_url_path
…' of https://github.com/pneumaticapp/pneumaticworkflow into frontend/configuration/46864__public_forms_via_url_path
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2eadc67. Configure here.
…' of https://github.com/pneumaticapp/pneumaticworkflow into frontend/configuration/46864__public_forms_via_url_path
…' of https://github.com/pneumaticapp/pneumaticworkflow into frontend/configuration/46864__public_forms_via_url_path
|
|
||
| return window.location.hostname.includes(formSubdomain); | ||
| }, | ||
| // Path-based forms: domain.com/forms/* |
There was a problem hiding this comment.
2 checks need to be combined because they do the same thing with 2 different paths
| const { formSubdomain } = getConfig(); | ||
| return req.hostname.includes(formSubdomain); | ||
| }, | ||
| // Path-based forms: domain.com/forms/* |
There was a problem hiding this comment.
2 checks need to be combined because they do the same thing with 2 different paths
| return window.location.hostname.includes(formSubdomain); | ||
| }, | ||
| // Path-based forms: domain.com/forms/* | ||
| check: () => !!getFormsBasename(window.location.pathname), |
There was a problem hiding this comment.
It's not quite clear what the check does, it's better to write the isFormPath utility where we already check all valid pathname or subdomains.
| config: { mainPage }, | ||
| } = getPublicFormConfig(); | ||
|
|
||
| const formsHistory = createBrowserHistory({ |
There was a problem hiding this comment.
If our forms are running on a subdomain, and getFormsBasename returns undefined, there is a chance that the history will not work correctly. Then you need to set '/' instead of undefined.

1. Problem
When navigating to a public form link
http://localhost/forms/{token}, the user was forcefully redirected to the sign-in page (/auth/signin) instead of seeing the public form.Where it manifested: any link of the form
domain.com/forms/{token}— the form never loaded, immediate redirect to login.2. Context
Previously, public forms worked only via subdomain (
form.domain.com). This required a separate DNS domain and complicated self-hosted deployments — not all installations have the ability to set up wildcard DNS or an additional domain.Switching to path-based mode (
domain.com/forms/...) simplifies self-hosted deployments where everything runs on a single domain. The subdomain mode remains backward-compatible for existing installations.Root cause: The frontend router (Express on server and React Router on client) was unaware of path-based forms — all traffic to
/forms/*hit the main application, which required authentication.3. Solution
Support for two modes of form access without breaking changes:
domain.com/forms/{token}— always availableform.domain.com/{token}— available whenFORM_DOMAINis setAt each stack level (nginx → Express → React Router),
/forms/*routing was added, and the app part identification logic (identifyAppPart) was extended for path-based detection.4. Implementation Details
nginx/templates/nginx.conf.templatelocation /forms/— proxy to frontendfrontend/src/server/server.tsapp.use('/forms', formsRouter)— path-based route (always); subdomain — conditional whenFORM_DOMAINis setfrontend/src/public/utils/identifyAppPart/constants.tsgetFormsBasename(pathname)— pure, testable utility for determiningbasenamefrontend/src/public/forms.tsxcreateBrowserHistory({ basename: getFormsBasename(...) })— React Router correctly strips/formsidentifyAppPartOnClient.tsgetFormsBasename) and subdomain (hostname.includes)identifyAppPartOnServer.tsreq.baseUrl === '/forms') and subdomaindocker-compose.yml,docker-compose.src.ymlFORMS_URLdefaulthttp://localhost/forms,FORM_DOMAIN— empty stringbackend/src/settings.pyCORS_ORIGIN_WHITELIST— safe origin extraction fromFORMS_URL(supports path-based URLs)mainHandler.test.ts,authMiddleware.test.tspath,baseUrl,cookieproperties to Express mock objectsNew test files:
getFormsBasename.test.ts— 20 test casesidentifyAppPartOnClient.test.ts— client-side detection testsidentifyAppPartOnServer.test.ts— server-side detection tests5. What to Test
5.1 Preconditions
docker-compose up)FORM_DOMAINis not set (empty string by default) — verifies path-based mode5.2 Positive Scenarios
Path-based form access
http://localhost/forms/{token}Embedded form via path
http://localhost/forms/{token}/embedMain application is unaffected
/dashboard,/templates,/tasksSubdomain mode (when
FORM_DOMAINis set)FORM_DOMAIN=form.localhosthttp://form.localhost/{token}Both modes simultaneously
FORM_DOMAINset, verify bothdomain/forms/{token}andform.domain/{token}5.3 Negative Scenarios and Edge Cases
Non-existent token
http://localhost/forms/nonexistent-token-123Path /forms without token
http://localhost/formsandhttp://localhost/forms/Similar paths don't break
http://localhost/formationsorhttp://localhost/formsubmitCase sensitivity
http://localhost/Forms/{token}orhttp://localhost/FORMS/{token}5.4 Verification Points
/api/..., not/forms/api/...5.5 API Checks
/api/v2/...— check Network tab5.6 What Was NOT Tested
6. Affected Areas (dependencies)
identifyAppPart(used inserver.ts,mainHandler,authMiddleware)formsRouter(Express)/static/,/favicon.ico) are not intercepted by/forms/settings.py)7. Refactoring
getFormsBasename— extracted path-based detection logic into a pure function (from inline code informs.tsxand duplication inidentifyAppPartOnClient.ts)identifyAppPartOnClient/OnServer— separated two checks (path-based and subdomain) for claritypath,baseUrl,cookie)Additionally verify: existing flows (guest-task, auth, OAuth) work without regressions.
8. Commits
All changes are in the current working tree. Files:
Modified (11):
backend/src/settings.py,docker-compose.src.yml,docker-compose.yml,frontend/src/public/forms.tsx,constants.ts,identifyAppPartOnClient.ts,identifyAppPartOnServer.ts,server.ts,mainHandler.test.ts,authMiddleware.test.ts,nginx.conf.templateNew (3):
getFormsBasename.test.ts,identifyAppPartOnClient.test.ts,identifyAppPartOnServer.test.ts9. Release Notes
Note
Medium Risk
Changes how unauthenticated traffic is classified (forms vs main app) across nginx, Express, and React; empty
FORM_DOMAIN/FORMS_URLdefaults alter self-hosted behavior until env is set explicitly.Overview
Public forms can be served at
/forms/{token}on the main site, withform.domain.comstill supported whenFORM_DOMAINis set.Routing and app detection: Nginx proxies
/forms/to the frontend; Express always mounts the forms router at/formsand only enables subdomain forwarding whenformSubdomainis configured. Client and serveridentifyAppPartcheck path-based/formsfirst, then subdomain hostname. The forms SPA usesgetFormsBasenameso React Router strips/formsin path mode and leaves routes unchanged on a forms subdomain.Config defaults: Docker clears default
FORMS_URL/FORM_DOMAIN(subdomain off unless set); Django derivesFORMS_URLasFRONTEND_URL/formswhen unset and adds a separate CORS origin only when the parsed forms origin differs from the frontend.Tests: Coverage for basename detection, client/server app-part identification, and Express handler mocks (
path,baseUrl,cookie).Reviewed by Cursor Bugbot for commit 0d5a052. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add path-based public forms routing under
/forms/as an alternative to subdomain mode/forms/*on the main domain in server.ts, in addition to the existing subdomain-based routing.getFormsBasename()in constants.ts to detect path-based forms and return the correct router basename; client and server app-part identification updated to check path first./forms/location block proxying to the frontend upstream in nginx.conf.template.FORMS_URLnow defaults toFRONTEND_URL + '/forms'when not set, andCORS_ORIGIN_WHITELISTis built from parsed origins to avoid duplicates.FORM_DOMAINdefaults to empty in docker-compose, disabling subdomain mode by default; subdomain routing is preserved whenformSubdomainis explicitly configured.Macroscope summarized 0d5a052.