Summary
Implement domain-based access control to separate public survey functionality from admin interface using different domains. This allows organizations to restrict admin access internally while keeping public surveys accessible externally.
Problem Statement
A customer requested the ability to have separate domains for:
- Admin Domain (survey-admin.example.com): Internal access only, used for dashboard, management APIs, and administrative functions
- Public Domain (survey-public.example.com): External access allowed, limited to public survey pages and client APIs for SDK integration
The current SURVEY_URL implementation doesn't provide a hard separation between these functionalities.
Solution Overview
Modify the middleware to implement domain-based routing that:
- Identifies the requesting domain
- Allows/blocks access based on domain and route combination
- Maintains full backward compatibility with existing single-domain deployments
Architectural Overview
Public Domain (PUBLIC_URL)
- Accessible Routes:
/s/[surveyId]
/c/[jwt]
/api/v1/client/**
/api/v2/client/**
/share/**
/health
- Purpose: Serve public-facing survey pages and client APIs.
Admin Domain (WEBAPP_URL)
- Accessible Routes:
/environments/**
/auth/**
/setup/**
/organizations/**
/product/**
/api/v1/management/**
/api/v2/management/**
/health
/ (root)
- Purpose: Provide access to administrative dashboards and management APIs.
Exceptions & Notes
- Static assets (e.g.,
/_next/static, /favicon.ico) must be served on both domains.
- The
/health endpoint should be available on both domains to support monitoring from both internal and external sources.
- Only routes passing through the middleware can be restricted; any uncategorized routes (such as cron or pipeline jobs) should default to being available only on the admin domain.
Task Breakdown
The following tasks (each tracked as a sub-issue) are all required to ensure robust domain-based access control.
1. Environment Variable Configuration(#5965)
To enable domain-based access control, we must introduce the PUBLIC_URL environment variable and remove the deprecated SURVEY_URL. This change forms the foundation for all subsequent routing and separation logic, ensuring the app knows which domain is public and which is for admins.
2. Middleware Implementation(#5966)
The middleware is the core logic enforcing the domain-based separation. It routes requests to either the public or admin functionality based on the request’s domain, blocking access where appropriate and ensuring a strict boundary between public and internal features.
3. API Endpoint Reorganization(#5962)
API endpoint /api/v1/og needs to be moved under the client API namespace to match the new public/admin split. This ensures that only the intended APIs are exposed on each domain, keeping the separation clean and predictable.
4. Move avatar uploads to private storage(#5980)
Update: We will remove this functionality soon, please use the public_domain for the avatar assets for now.
We are moving the path of public assets(uploads) to the public_domain, to keep the separation between public and private assets. We store the avatar images in the public directory, but the avatars should not be publicly accessible and can be moved to private storage for consistency.
5. Documentation (#5968)
Update the documentation to reflect the new domain-based access control setup and add a section on how to configure the domains, and the routes that are available on each domain.
Configuration Examples
Default Setup (Most Customers)
# No changes needed - current behavior maintained
WEBAPP_URL=https://app.formbricks.com
# PUBLIC_URL not set
Domain Separation Setup (New Feature)
# Admin domain (internal access)
WEBAPP_URL=https://survey-admin.example.com
# Public domain (external access) - with enforced route separation
PUBLIC_URL=https://survey-public.example.com
# DNS/Network setup required:
# - survey-admin.example.com → internal/VPN access only
# - survey-public.example.com → public internet access
Testing Strategy
- Backward Compatibility: Verify that existing single-domain deployments function without changes when
PUBLIC_URL is unset.
- Domain Separation: Test that public and admin domains serve only their respective routes.
- Error Handling: Ensure that unauthorized access attempts result in appropriate error responses (e.g., 404).
Out of scope
- We are not modifying the one click setup or basic helm chart configuration as this is a niche feature only used by a very small number of enterprise customers managing their own infrastructure to set up the desired behavior.
Acceptance Criteria
Summary
Implement domain-based access control to separate public survey functionality from admin interface using different domains. This allows organizations to restrict admin access internally while keeping public surveys accessible externally.
Problem Statement
A customer requested the ability to have separate domains for:
The current
SURVEY_URLimplementation doesn't provide a hard separation between these functionalities.Solution Overview
Modify the middleware to implement domain-based routing that:
Architectural Overview
Public Domain (
PUBLIC_URL)/s/[surveyId]/c/[jwt]/api/v1/client/**/api/v2/client/**/share/**/healthAdmin Domain (
WEBAPP_URL)/environments/**/auth/**/setup/**/organizations/**/product/**/api/v1/management/**/api/v2/management/**/health/(root)Exceptions & Notes
/_next/static,/favicon.ico) must be served on both domains./healthendpoint should be available on both domains to support monitoring from both internal and external sources.Task Breakdown
The following tasks (each tracked as a sub-issue) are all required to ensure robust domain-based access control.
1. Environment Variable Configuration(#5965)
To enable domain-based access control, we must introduce the
PUBLIC_URLenvironment variable and remove the deprecatedSURVEY_URL. This change forms the foundation for all subsequent routing and separation logic, ensuring the app knows which domain is public and which is for admins.2. Middleware Implementation(#5966)
The middleware is the core logic enforcing the domain-based separation. It routes requests to either the public or admin functionality based on the request’s domain, blocking access where appropriate and ensuring a strict boundary between public and internal features.
3. API Endpoint Reorganization(#5962)
API endpoint
/api/v1/ogneeds to be moved under the client API namespace to match the new public/admin split. This ensures that only the intended APIs are exposed on each domain, keeping the separation clean and predictable.4. Move avatar uploads to private storage(#5980)
Update: We will remove this functionality soon, please use the public_domain for the avatar assets for now.
We are moving the path of public assets(uploads) to the
public_domain, to keep the separation between public and private assets. We store the avatar images in the public directory, but the avatars should not be publicly accessible and can be moved to private storage for consistency.5. Documentation (#5968)
Update the documentation to reflect the new domain-based access control setup and add a section on how to configure the domains, and the routes that are available on each domain.
Configuration Examples
Default Setup (Most Customers)
Domain Separation Setup (New Feature)
Testing Strategy
PUBLIC_URLis unset.Out of scope
Acceptance Criteria
PUBLIC_URLis not set: all routes work onWEBAPP_URL(current behavior)PUBLIC_URLis set: domain separation is enforced/s/,/c/), client APIs (/api/v*/client/), and share survey results (/share/<id>)PUBLIC_URLis not set