So I’ve been tearing my hair out the past week because I’m building a project using Remix in my free time, and it doesn’t support multi-tenancy.
So what exactly is multi-tenancy?
It’s the ability to serve multiple domains from a single app.
Essentially my app is a website builder. So the builder is on my domain. Clients will sign up and point their own domain to the app. I will also host their pages on theirname.myapp.com.
The problem with this is, I need a marketing page on myapp.com. Then i need completely different code if the domain is clientsite.com.
In remix you can check the HTTP host header and render different things, but this is not ideal. If my app has `routes/about.tsx` I want the client to be able to have their own `about` page.
I don’t want to check the host header in my routes and then make a mess of the code.
Not only that, but you will end up bloating your JS bundle with internal app code vs client page code.
How to do it
Let’s just jump right into how I made this happen in Remix.
At first, I tried to put a folder at `routes/landing-pages` and then fake the url. So if the host header is not my app, prefix the url with `landing-pages` so remix thinks someone is requesting `/landing-pages`.
This would be perfect, so I can have all my personal app routes in `/routes/anything` and then all client routes stored in `/routes/landing-pages`.
This actually worked… on the server. Unfortunately there is no hook into the browser history inside of the client side Remix entry point. So I couldn’t get that working without patching the npm module.
A day went by and I was stumped. Until I realized, remix has adapters for express! That means we can make a `server.js` file and make this happen.
I’m using the normal node adapter currently, this solution won’t work if you are deploying to serverless, although it may if you adapt it to the remix adapter you are using.
Here’s the server.js in the root of my app:
let express = require("express");
const remix = require("@remix-run/express");
const app = express();
const isClientSite = (req) => {
if (
req.headers["host"] !== "localhost:3000"
)
return true;
return false;
};
app.use(express.static("public"));
app.use("/build", (req, res, next) => {
if (isClientSite(req)) {
express.static("public/build")(req, res, next);
} else {
express.static("public/build-main")(req, res, next);
}
});
app.all("*", (req, res, next) => {
if (isClientSite(req)) {
return remix.createRequestHandler({ build: require("./build") })(
req,
res,
next
);
}
return remix.createRequestHandler({ build: require("./build-main") })(
req,
res,
next
);
});
app.listen(process.env.PORT || 3000, () => console.log("server started!"));
Let’s break this down.
let express = require("express");
const remix = require("@remix-run/express");
const app = express();
const isClientSite = (req) => {
if (
req.headers["host"] !== "localhost:3000"
)
return true;
return false;
};
The initial setup. This function just checks the incoming http host header. If it’s not equal to your app’s domain, then it returns true, meaning this is a client’s site!
Replace this with your app’s domain name.
If you point `test.com` in my `/etc/hosts` file, you will be able to hit the server and test that its working locally.
Next, this section:
app.use(express.static("public"));
app.use("/build", (req, res, next) => {
if (isClientSite(req)) {
express.static("public/build")(req, res, next);
} else {
express.static("public/build-main")(req, res, next);
}
});
Remix has a public folder, and it puts a build folder inside of it. To make this solution work, we will actually call remix build twice, once with your main app’s routes, and another with the client routes. So we have to add the check here so the client side can find the JS bundles from the correct build folder.
And finally!
app.all("*", (req, res, next) => {
if (isClientSite(req)) {
return remix.createRequestHandler({ build: require("./build") })(
req,
res,
next
);
}
return remix.createRequestHandler({ build: require("./build-main") })(
req,
res,
next
);
});
app.listen(process.env.PORT || 3000, () => console.log("server started!"));
We serve remix from the build folder when it’s a client site, and from build-main when it’s not.
Now you are wondering, how did we make two build folders!
Setup your routes folder
Before we can do that last piece, you need your routes folder to be like this:
routes/__app
routes/clients
Remix has a really nice convention where we are able to prefix a folder by two underscores, and it doesn’t actually make the path different. `routes/__app/index` will be served when the url path is `/`
Put all your app’s routes inside __app. Put all your client site code into /clients.
Finally, we update our build script to do this:
remix build &&
mv build build-main &&
mv public/build public/build-main &&
rm -rf app/routes/__app &&
mv app/routes/clients app/routes/__clients &&
remix build
We are making a normal build. Then we have to move the build folders so they don’t get overwritten by our second client’s build.
Now, we remove __app, and rename clients to __clients, so all the routes are on the root.
We run the build again, we have both build folders.
If you have any questions please leave them :)
Super helpful! Any ideas how to support custom client domains?
clientdomain.com => client.app.com
hey nice one, been trying to get something similar working, I had 2 questions;
1 - any chance of making an example repo?
2 - with the part of the build script `... rm -rf app/routes/__app && ...` how are you restoring your `__app` folder?