- An internal marketplace operator and an external supplier do not use the same product.
- The operator reviews suppliers, manages organisations, investigates workflows and governs the catalogue.
- Giving both audiences the same navigation with a few hidden links creates a confusing product.
- For a marketplace running on Medusa Cloud, we built two distinct React applications inside one custom dashboard delivery.
The client problem
An internal marketplace operator and an external supplier do not use the same product.
The operator reviews suppliers, manages organisations, investigates workflows and governs the catalogue. The supplier imports products, manages orders, updates commercial data and follows the work relevant to its own business.
Giving both audiences the same navigation with a few hidden links creates a confusing product. Building two completely independent frontends creates another problem: two deployment pipelines, two environment matrices, two asset configurations and two release trains attached to one commerce backend.
For a marketplace running on Medusa Cloud, we built two distinct React applications inside one custom dashboard delivery. Dedicated admin and vendor hostnames select the right panel. Each panel owns its router and product surface. Legacy /app/admin and /app/vendor links continue to work. Medusa's backend-controlled Vite build ships the bundle with the server.
The client gets two operational products without paying the operational cost of two unrelated frontend platforms.
Replace the dashboard entry, not the commerce engine
Medusa already owns an admin application build within the backend deployment. The project uses that extension point rather than standing up an unrelated static-hosting architecture.
In the backend configuration, the Medusa dashboard package entry is aliased to the project's own React entry file. Dashboard CSS, route aliases, shared components and public assets are resolved from the custom dashboard workspace. The backend prebuild prepares the dashboard before the Medusa build runs.
Medusa Cloud therefore remains responsible for building and serving the administrative frontend artefact. The custom code controls what application starts inside that artefact.
This keeps the release unit coherent. A backend change and the panels designed for it travel through the same Cloud build. The marketplace does not need a second hosting provider merely because its operational interface is richer than the default admin.
Select the product from the public hostname
The shared entry point inspects the browser hostname before mounting React.
An admin.* host selects the internal administration application. A vendor.* host selects the supplier application. On those dedicated hosts, both products live at / from the user's perspective.
The hostname is a clean product boundary for navigation and branding. Suppliers do not see an internal-looking /app/vendor prefix. Operators receive an equally direct admin URL. Bookmarks and copied links describe the product people are actually using.
If the browser is on the older shared host, the entry falls back to the path. /app/admin selects administration and /app/vendor selects the supplier panel.
If neither a recognised hostname nor a legacy path identifies a product, the entry chooses an explicit fallback instead of rendering an accidental mixture.
Give each panel its own router root
React Router needs to know which part of the browser path belongs to the application.
On a dedicated hostname, the selected panel receives / as its basename. A supplier order can therefore live at /orders/..., and an admin workflow can live at /workflow-executions/....
On the shared compatibility host, the same supplier router receives /app/vendor; the admin router receives /app/admin. The route definitions inside each application do not need two copies. The entry supplies the correct external base for the current delivery context.
This is more than URL polish. Hard-coded path prefixes spread quickly through login redirects, product links and navigation helpers. Passing the basename into each application keeps the routing contract close to the deployment decision.
Both products can evolve their own route maps while the shared entry remains responsible only for selecting and mounting one of them.
Preserve existing deep links during the hostname migration
Changing from shared paths to dedicated subdomains can break links already stored in emails, documentation and browser bookmarks.
The entry handles the mixed case deliberately. If an admin hostname receives an old /app/admin/... URL, it strips only that prefix and redirects to the remaining path on the same host. The vendor hostname does the equivalent for /app/vendor/....
The remainder of the path and the query string are preserved. A link to a specific order or invitation does not collapse to the panel homepage merely because the delivery model changed.
Meanwhile, the old shared-path URLs remain valid for compatibility. Backend helpers can use explicit public panel URLs when configured and retain legacy defaults where necessary.
This allows migration to happen without a flag day. New links can use the dedicated product hosts while historical links continue to reach the intended screen.
It also avoids asking support to diagnose a routing migration as though it were an authentication incident. When an old notification link lands on the new host, the application can translate the delivery convention before login and routing continue. The user reaches the intended business object, and the team can retire old link producers gradually instead of coordinating every email template, bookmark and document for one release date.
Load only the application the user asked for
The entry does not eagerly import both complete panels before deciding which one to render.
After resolving admin versus vendor, it uses a lazy import for the selected sub-application. Vite can place those entry branches in separate chunks, and the browser follows the branch required for the current host.
This protects the conceptual boundary as well as initial loading. The supplier product does not need to initialise the internal admin application, and the admin product does not need to mount supplier routes.
The evidence supports lazy entry loading, not a claim that the unused product contributes literally zero bytes to every generated artefact. Shared runtime and common dependencies still exist. The meaningful design decision is that application initialisation follows the chosen product rather than booting both and hiding one.
Keep authorisation on the server
A hostname, lazy import or hidden navigation item is not a permission system.
The admin and vendor panels create the correct user experience, but backend authentication, actor type, tenant scope and API middleware must still decide what data and actions each user may access.
This matters precisely because both panels ship from one delivery artefact. Anyone can inspect frontend code or attempt a URL. The platform remains safe only when admin APIs require admin authority and supplier APIs enforce the supplier's scope.
The frontend boundary answers: which product should this user experience? The backend boundary answers: which operations and records is this identity authorised to use?
Keeping those responsibilities separate lets the team optimise delivery without weakening marketplace isolation.
Inject configuration where the Cloud build actually runs
A local dashboard Vite configuration is not necessarily the configuration used in production.
Medusa Cloud initiates the admin build from the backend. Variables, aliases and compile-time definitions required by the custom panel must therefore be supplied through the Medusa admin Vite hook, where that build can see them.
The project defines the application base, backend relationship, storefront URL, environment label and selected integration keys there. It also points static assets at the dashboard's public directory and lists dependencies the build should pre-optimise.
Assets follow the same rule. Because Medusa serves the dashboard beneath /app in the shared setup, the favicon is resolved from the compiled base rather than assuming /favicon.ico at the origin root.
Configuration belongs to the system that performs the production build, not merely the tool developers run locally.
Why this architecture suited Medusa Cloud
The marketplace needed custom operational products, but it did not need a second commerce runtime.
Medusa's dashboard build hook made the backend deployment an effective shell for those products. React and Vite provided separate application entry branches, routers and shared components. Hostname routing made each panel feel independent to its audience.
The architecture reduced deployment surface while preserving product clarity. Backend and dashboard contracts move together. Shared design foundations stay close. Operators and suppliers receive URLs and navigation designed for their work.
There is another practical benefit: preview and test environments inherit the same relationship. A branch does not need two independently configured frontend previews to exercise a backend capability across both audiences. The release candidate contains the admin and supplier experiences expected by that exact server version. That does not remove the need to test both products, but it makes the unit under test explicit and reproducible.
This is the architectural choice: consolidate what is operationally shared, separate what is experientially and securely distinct.
A two-panel delivery checklist
Before combining operational panels into one build, ask:
- Are the audiences doing genuinely different jobs?
- Does each product own a separate route tree?
- What trusted signal selects the application?
- Does each dedicated host receive a clean root basename?
- Which legacy paths must remain valid?
- Are deep-link paths and query strings preserved during migration?
- Is only the selected application initialised?
- Which components are truly shared concepts?
- Where does the production Vite build receive its variables?
- Does the public asset base match how Medusa serves the dashboard?
- Can backend-generated links target the right public panel?
- Are admin and supplier permissions enforced server-side?
- Can one release update backend and panels coherently?
- Is the fallback for an unknown host or path explicit?
The answer should reduce operating burden without blurring product or security boundaries.
The broader lesson
Two products do not always require two deployment platforms.
For this marketplace, we built a shared Medusa Cloud dashboard delivery that selects a dedicated internal or supplier application by hostname, keeps clean roots on the new domains and preserves legacy paths for existing links.
Each audience receives a focused product. The engineering team retains one build and release unit. Shared foundations remain reusable. Backend authorization remains authoritative.
That is the balance worth designing for: fewer moving parts for the team running the platform, with no ambiguity for the people using it.
