Multiple apps on a single domain hosted on sub-paths
tl;dr It's possible to deploy multiple apps on the same domain by serving each at a different sub-path, using a simple vercel.json configuration, and custom build commands.
Even a static SPA React app with client-side routing can be configured to be served at a custom path.
When developing my llama farm chat demo, I had been developing it like a normal app, with the index at /
on http://localhost:5173/
, but then needed to host the app entirely under the /llama-farm
sub-path at labs.convex.dev/llama-farm, since labs.convex.dev also hosts auth, Ents, a million checkboxes clone and other independent projects. It uses a client-side router (React Router), which needs all routes to get redirected to the same index.html
, and each page needs to get served relative to that subpath, without littering the codebase with a prefix for every route.
In this post I'll cover:
- Hosting your Vite-based React app with client-side router under a sub-path on Vercel.
- Hosting multiple apps on subpaths within a single project.
- Configuring multiple sub-paths for the same app in a single vercel.json.
Serving on a subpath
To change my app to work on a sub-path on Vercel, the four changes that I needed to make were:
- Setting the public base path by specifying
--base=/llama-farm
in myvite build
command. - Setting the
build.outDir
to--outDir=dist/llama-farm
so it would create the assets at the relative path Vercel expected. You can see the full build command below. - Setting the
basename
field for React Router oncreateBrowserRouter
to{ basename: "/llama-farm" }
. In order to test locally at/
I made this an environment variableVITE_BASEPATH
that I only set in Vercel. You can see my code here. - Adding a
rewrites
configuration to myvercel.json
file:
{
"rewrites": [
{
"source": "/llama-farm(.*)",
"destination": "/llama-farm"
}
]
}
On Vercel my build command ended up looking like:
vite build --base=$VITE_BASEPATH --outDir=dist$VITE_BASEPATH
I set the VITE_BASEPATH
environment variable to /llama-farm
so it was shared between the build command and React Router config.
Adding in Convex
One more layer is if our app uses Convex for the backend, in which case we can also deploy our Convex backend at the same time. For this, we wrap our build command with npx convex deploy --cmd '<build command here>'
.1 This will build the Vite app with the VITE_CONVEX_URL
set, which tells the frontend where to find the corresponding backend. This is especially useful for preview deploys which each have their own backend. As an additional safeguard, if the build fails it won't deploy your backend.
The full updated build command:
npx convex deploy --cmd 'vite build --base=$VITE_BASEPATH --outDir=dist$VITE_BASEPATH'
Now my chat app is being served on llama-farm-chat.vercel.app/llama-farm
. In the next section I'll connect it to the app running labs.convex.dev
so I can access it on labs.convex.dev/llama-farm
.
Serving multiple supaths from another project
To serve my app from the labs.convex.dev
app, I'll edit the vercel.json
for the Convex Labs project to handle redirecting and rewriting requests to my app.
redirects
will serve assets from the same relative path, but from elsewhere. In my case the.js
and.css
files are in thellama-farm/assets/
directory.rewrites
will take into account the rewrites in my llama farm project, both for the base url/llama-farm
and any subpaths. These will all get rewritten to/llama-farm
which will serve theindex.html
file there.
{
"redirects": [
{
"source": "/llama-farm/assets/:filePath",
"destination": "https://llama-farm-chat.vercel.app/llama-farm/assets/:filePath",
"permanent": true
},
//... other projects
],
"rewrites": [
{
"source": "/llama-farm",
"destination": "https://llama-farm-chat.vercel.app/llama-farm"
},
{
"source": "/llama-farm/:match*",
"destination": "https://llama-farm-chat.vercel.app/llama-farm/:match*"
},
//... other projects
]
}
You can read more about redirects and rewrites in the Vercel docs.
Configuring multiple subpaths
You might want to build the same project and host it on different sub-paths on different domains. For example, I also host my app at llamafarm.chat.
I'll start by saying there are many Vercel community discussions about how using environment variables to configure vercel.json is not supported. The recommendation is to use edge function middleware or a Next.js config. For this app I'd rather not pay for serverless functions or migrate it to Next.js.
However, I have a configuration that is working for me. I've found that I can put both rewrite configs into one vercel.json
and both apps will route correctly.
vercel.json
:
{
"rewrites": [
{
"source": "/llama-farm(.*)",
"destination": "/llama-farm"
},
{
"source": "/(.*)",
"destination": "/"
}
]
}
On labs.convex.dev/llama-farm, it makes sense that it will only ever match against incoming urls starting with /llama-farm
since the labs.convex.dev
project only rewrites those urls.
However, I'm a bit confused why llamafarm.chat correctly routes /llama-farm
to /index.html
instead of looking for /llama-farm
. My guess is that it can't find any /llama-farm/index.html
at build time, so ignores the first rule for that project. If you know more, please reach out! I'm just happy that it works.
Summary
We looked at hosting a client-routed SPA app (in my case a Vite app using React Router) on a base path, serving multiple apps on the same project, and configuring the rewrite paths for multiple environments in the one vercel.json
.
I hope it helps you out! Come chat in our community Discord if you have any comments or feedback.
Footnotes
-
Note when pointing multiple Vercel projects at one Convex backend: I choose to only deploy the Convex backend from one project, and just do the frontend-only deploy from the other. For the one that is only deploying the frontend, I set the
CONVEX_SITE_URL
environment variable instead of theCONVEX_DEPLOY_KEY
. This means that all frontend preview deployments for this project are talking to the prod backend. ↩
Convex is the sync platform with everything you need to build your full-stack project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.