sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
18.52k stars 1.91k forks source link

(Sub)Domain-based Routing #8335

Closed hrueger closed 1 year ago

hrueger commented 1 year ago

Describe the problem

Hi, first of all, thanks for this excellent piece of software! I truely love it.

I'm currently building a platform where I have multiple tenants. They can configure some settings and provide a frontend for their users. This should live on https://tenant-slug.my-platform.tld. Then, they should also have an admin-dashboard at https://admin.tenant-slug.my-platform.tld. I'll also have some registration / onboarding ui at https://registration.my-platform.tld.

Currently, this does not seem possible with a single Svelte app. I can't build one for each tenant, because they change quite often.

At DNS level it would be solved via a wildcard subdomain and a wildcard SSL certificate.

Describe the proposed solution

As proxies (see below) are complicated to setup and didn't work for my usecase, I'd like to be able to have different routes for each subdomain and the pure domain. Maybe this is kind-of linked: https://vercel.com/guides/nextjs-multi-tenant-application

Here are my thoughts: Currently, there's the routes directory with all the routes inside. In order to support multiple (sub)domains, there could be multiple routes folders like routes_registration., routes_[tenant]. and routes_admin.[tenant].. Notice the dot at the end, which means that it is a subdomain. Without the dot, we could support thinks like site-with-german-title.de and english-site.co.uk as then, it would match against the whole host.

The only thing I don't like about this, however, is the fact that we're breaking the convention, that each folder name equals one segment of the path exactly (as we add the routes_ prefix. One way to work around that would be to have a new folder called domains in the src directory, and inside there other folders with the names / wildcards of the subdomains. However, we'd then need a way to split that part from where the actual path-based routing begins. This could be done with a convention (like a folder named _routes in there), however, that would prevent people from having a _routes subdomain.

Please let me know what you think about that, I'm happy to hear other opinions!

Alternatives considered

What I've tried so far is using a reverse proxy (traefik in this case) to map the subdomains to path prefixes (e.g. registration.my-platform.tld to my-platform.tld/registration internally. This, however, does not work, as the base path is always wrong in that case. Maybe this is related to #595, but I don't think its the same case as with that proxy it would need a multiple different base paths (or rather hosts).

Importance

would make my life easier

Additional Information

This feature is rather important for my application, as there is the requirement for nice urls using subdomains.

I'm also happy to try implementing something once we have a proposed solution and someone can point me in the right direction.

Conduitry commented 1 year ago

If each subdomain's application has the same basic shape (which it should if you want to use a single application for all of them), then you should be able to just expose the Host header to the rest of your app either in your handle hook or in a top-level +layout.server.js.

hrueger commented 1 year ago

Hi @Conduitry, thanks for the fast response. I haven't thought about that. However, I don't think that it would work for the described usecase. The routes are not the same for all subdomains. There's the difference between user-frontend and tenant-admin-backend as well as the registration pages. They do share a lot of UI components and server logic though, so I'd prefer not to move that into a package make separate apps...

Edit: Logic in the +layout.server.ts file does not allow my to switch between bulks of routes dynamically, right?

hrueger commented 1 year ago

It seems possible with Nuxt: https://ynechaev.medium.com/subdomains-with-nuxt-js-483df826c788

georgeiliadis91 commented 1 year ago

I would +1 this, I am looking into making an application where each user has his own subdomain and having a build in way to get it would be really convenient. This is something that is supported in other frameworks as far as I remember.

Rich-Harris commented 1 year ago

The routes are not the same for all subdomains. There's the difference between user-frontend and tenant-admin-backend as well as the registration pages.

It sounds like you have three separate apps, two of which could use @Conduitry's idea of exposing Host...

...and you just need to route requests to one of the three apps based on the hostname.

Is there any reason you need those to instead be a single app?

mquandalle commented 1 year ago

I have the same use case : ~4 apps that share most of their UI components, stores, utils, but with different routes. The apps are deployed on different domains. (ex1, ex2)

The way I solve it currently is to have a monorepo with a routes directory per app, like routes/app1. and routes/app2 I then use the kit.files.routes config to set the correct directory depending on an env variable PUBLIC_APP=app1 npm run dev.

I deploy these apps on Vercel where I have 4 different "projects" connected to the same repo, but with a different env variable.

This works very well, except that now the deployment is slow. Vercel needs to rebuild the app 4 times and it isn't done in parallel (extra workers are costly on Vercel). If Svelte Kit supported this "multi-tenant application" pattern to build a single "app" to serve my 4 distinct sites, I would be happy to switch to it to simplify the development and deployment processes.

hrueger commented 1 year ago

Is there any reason you need those to instead be a single app?

I'd say it's about convenience for the developer. Svelte Kit is already so great at dev experience and I (and if I understand @mquandalle and @georgeiliadis91 correctly them, too) feel like being forced to split apps - just because the routing technique we'd like to use is partly based on (sub)domains and not exclusively on paths - impairs that quite a bit.

Advantages I see with a single app are the following:

Don't get me wrong. I could probably achieve the same thing with the current version of Svelte Kit, it would just not be nice code and a good dev experience. You could also completely ignore the router, parse the path segment of the URL and have a single template with a ton of {#ifs. It would just not be as convenient, usable and (most importantly) maintainable then using the router. I see the (sub)domain routing feature kind of related to this example.

Can you see my points?

Best regards

antony commented 1 year ago

Couldn't you just deploy a single app, and assign all of your desired subdomains to it in vercel's config?

then you can look at the host header in a hook, and just respond accordingly.

single app, single deployment, multiple assigned domains

supporting this sort of thing in SvelteKit, even though others have done, feels like the wrong thing to do. Domains are supposed to be different computers (based on the original design for domain names), and handling this within a framework like SK feels out of scope.

(oh and personally, for scalability and sanity, we have done this type of setup with a monorepo and multiple apps - it sounds like you're forcing a monolith where your architecture screams out for microfrontends)

mquandalle commented 1 year ago

Couldn't you just deploy a single app, and assign all of your desired subdomains to it in vercel's config?

I would like to, but I don't know how to benefit from file system routing for multiple apps simultaneously.

routes/
  app1.com/
  app2.fr/
  subdomain.app1.com/

Maybe it's possible to rewrite the query in a hook to append the host name to the query path so that http://app1.com/about is routed into /app1.com/about/? But then it also need to work in dev mode, probably by defining a base path, so that a <a href="/about" goes to localhost:5173/app1.com/about, which maybe need some support from Svelte Kit?

(oh and personally, for scalability and sanity, we have done this type of setup with a monorepo and multiple apps - it sounds like you're forcing a monolith where your architecture screams out for microfrontends)

For instance I use it for app variants for specific countries : france, italy, where I want to customize the routes and the content for each country but still reuse most of the functionalities. Another example : https://mesaidesvelo.fr and https://aideretrofit.fr which are based on the same repo using the kit.files.routes parameter I mentioned earlier https://github.com/mquandalle/mesaidesvelo/blob/master/svelte.config.js#L10

Rich-Harris commented 1 year ago

Honestly, this feels like 'scenario solving' to me. Bottom line, these are separate apps and should be treated as such. Introducing significant extra complexity to the routing system just to cut down on build time a bit is entirely the wrong trade-off.

'I don't want to set up a monorepo' simply isn't a good enough reason to make the framework itself more complicated. This is what monorepos are for.

There are workable approaches to do the things described in this thread, so I'm going to close this issue.

hrueger commented 1 year ago

OK, if you say that's out of scope, we need to accept that.

@mquandalle Did you manage to get your approach (multiple routes folders and switching them with an env variable) working with the VSCode Intellisense? I've just tried it and ran into a lot of problems. When having a single .svelte-kit directory, multiple processes try to write to the same file (I need to have all apps running when developing). When having different .svelte-kit folders, the Intellisense doesn't like that, as the tsconfig.json at the root needs to point to one of the generated tsconfig.jsons explicitely...

hrueger commented 1 year ago

@Rich-Harris is there an example of a monorepo available? I'm struggeling with some points. For example: a .env file at the workspace root is not parsed for $env/static/private. Copying the .env file to all the child packages seems kinda redundant...

Edit: Seems like even copying does not work if lib is a folder in the workspace (with shared logic). I guess I need to manually load the .env file for shared logic.

Edit2: Seems like I can't overwrite $lib in the alias config. I can use something like $myLib to point to the shared lib folder, but then I loose the svelte kit warning in case I'd import $lib/server in client-facing code... That's a bummer

antony commented 1 year ago

Hi @hrueger - please use discord (https://svelte.dev/chat) for usage questions, where people will be better able to discuss and help with your specific requirements.

MentalGear commented 1 year ago

Discord is fun, but it's hard to find things on it and it's closed (you need a login). I always find it much better if there's a proper documentation on publicly available sites like github for issues like these that many people might want to google for.

ravi-ojha commented 5 months ago

While the debate is settled, I was exploring this as I had a small personal use.

@hrueger How about handling subdomain via hooks.

My usecase was very specific and personal. This code also handles just that. It's not generic and I do not recommend you use this for production systems without due diligence.

// src/hooks.ts
import { redirect } from '@sveltejs/kit'
import type { Handle } from '@sveltejs/kit'

export const handle: Handle = async ({ event, resolve }) => {
  const host = event.request.headers.get('host')
  const url = new URL(event.request.url)

  // Check if the request is from a subdomain in both localhost and production
  if (host && url.pathname.slice(0, 5) !== '/help') {
        // Assuming production domain is 'example.com'
        if ((host !== 'localhost:3000' && host.endsWith('localhost')) ||
            (host !== 'example.com' && host.endsWith('support.example.com'))) {
            // Redirect to '/help' when accessed via any subdomain
            throw redirect(307, '/help');
        }
    }

    // Continue with normal flow if no subdomain
    return resolve(event);
};
zicklag commented 3 months ago

This is super easy to do with the reroute hook:

I'm planning on using it for the "each user has their own subdomain" use-case.

jerriclynsjohn commented 1 month ago

This is super easy to do with the reroute hook:

I'm planning on using it for the "each user has their own subdomain" use-case.

This is excellent, but I wish if we could've accessed DB in this hook. Every reroute requires you to know them ahead of time, we can't do anything dynamic. Especially, looking up custom domains is not at all possible.

How do you implement Vercel Platforms like application in SvelteKit now? Whatever examples I saw are not what I'm looking at.

zicklag commented 1 month ago

You can see how I do it in my app here if you want: https://github.com/muni-town/weird/blob/main/src/hooks.ts

You don't need to access the database in that hook. What you do is you check if it's a subdomain, and then you route to a subdomain specific route in your app.

Then when it gets to that route, your route checks the database to see whether or not that subdomain exists in the database or should return a "page not found" because it's not there.

jerriclynsjohn commented 1 month ago

@zicklag This is true, and a wonderful growth hack to get users to create a profile under your subdomain. It's perfect, but imagine you gave your users the ability to point their custom domain, now unless we know that it came from a specific domain that is valid (by checking the DB), we shouldn't rewrite them to the path. Given what you have already implemented, how will you go about adding custom domain into your product.

zicklag commented 1 month ago

I actually have custom domains working, too!

Basically you could do something like:

Then, in our /subsite/[usernameOrCustomDomain] route we check the database to see if usernameOrCustomDomain is a username in our database. If it is, we render a subdomain site from the user's data.

If usernameOrCustomDomain isn't a username, we look in our database for a registered custom domain, and we render the user's site that has that custom domain set.

If there is no matching sub-domain, we just return a 404.


I also some stuff in there, when the user is configuring their custom domain, it does a little DNS "challenge" to make sure that the domain they are inputting is actually resolving to to the app server, but that's just around how you set the domain, not how you do that routing.

There's also some stuff in there to generate a Traefik reverse proxy configuration for each custom domain, so that we can provide automatic HTTPS certificates for everything. But again, that's kind of unrelated to the routing.


The code is running at https://weird.one/members.

And you can see a subdomain site: https://zicklag.weird.one And a custom domain site: https://erlend.sh

jerriclynsjohn commented 1 month ago

@zicklag dude this is genius. Dayum

RonenEizen commented 1 month ago

Another solution could be to have a separation at the configuration level. You can create a separate config file for each sub-app and specify parameters such as files, appDir, etc in there. Then you create a separate deployment/development scripts for each app.

RonenEizen commented 1 month ago

Another solution could be to have a separation at the configuration level. You can create a separate config file for each sub-app and specify parameters such as files, appDir, etc in there. Then you create a separate deployment/development scripts for each app.

Apparently separate config file are not supported. The approach is still valid though by using command args and detecting them within the config file as it is shown in this example: https://github.com/sveltejs/kit/issues/2973#issuecomment-986050439