How to Configure Tutor for Multi-Tenant or Microsite Support with MFE?

Hi everyone,

I’m currently working on a multi tenant support using Tutor (with Sumac Open edX release), and I’m trying to configure it to support multi-tenancy or microsites.

In earlier versions of Open edX, there was native support for microsites/multi-tenancy via theming and site configurations. However, with the transition to Micro Frontends (MFEs) in recent versions, I haven’t been able to find any updated or working guidance on how to achieve this.

Has anyone managed to configure Tutor for multi-tenant setups using MFEs? If so:

  • How did you handle the theming per site?

  • Did you use custom domain mappings?

  • Were any plugins or workarounds required?

Any guidance, examples, or pointers would be greatly appreciated!

Thanks in advance!

1 Like

Hi @rajpipaliya048 ! I’ve changed the category of your post from Community to Site Operations > Site Operations Help. It’s a good idea to choose the proper category for your post to get the right eyes on it. :slight_smile:

1 Like

Hi @rajpipaliya048
I’ve been working on a similar setup and can share a few things that may help.

For full multi-tenancy support in Tutor (especially with MFE support), I strongly recommend using the eox-tenant plugin. It’s much more flexible than relying solely on configuration_helpers.get_value, since it allows you to override Django settings per tenant, making tenant-specific behavior much easier to manage.

:wrench: Theming and MFE Customization

MFE theming is indeed limited. However, you can manage some tenant-specific branding using the mfe_config_api. Combined with eox-tenant, this allows you to control things like:

  • Logos
  • Titles
  • Other basic settings per site

However, dynamic changes to headers, footers, or component structure based on the tenant are not currently supported (at least not to my knowledge).

For styling, you can use Design Tokens — which, if I’m not mistaken, are now merged into the main branch of Paragon. I’m not entirely sure if all MFEs support them yet, but at least frontend-app-learning already includes them. This allows some degree of tenant-specific visual customization via CSS.

Domain Handling

There are many ways to handle domain separation, but what I’ve implemented involves using Caddy (which Tutor uses) to route requests by domain to CloudFront, where static MFEs are hosted.

Here’s an example rule that routes all traffic for a tenant (e.g., sitea.domain.com) to CloudFront, removing the apps. prefix (which caused more issues than it solved):

*.domain.com{$default_site_port} {

    # MFE-multitenant
    @learning_mfe_matcher {
        path /learning/*
    }
    route @learning_mfe_matcher {
        reverse_proxy https://my.cloudfront.net {
            header_up Host {http.reverse_proxy.upstream.hostport}
            @proxy_redirect status 404 403
            handle_response @proxy_redirect {
                rewrite * /learning/index.html
                reverse_proxy https://my.cloudfront.net {
                    header_up Host {http.reverse_proxy.upstream.hostport}
                }
            }
        }
    }

    import proxy "lms:8000"
}

This rule allows each subdomain (e.g., sitea.domain.com, siteb.domain.com) to serve a shared or separate MFE from CloudFront, while still routing API requests back to the correct backend via Tutor.

Since each tenant loads different settings via eox-tenant, the frontend will reflect tenant-specific configuration (e.g., available fields, options, logos, etc.) even when sharing the same MFE build.

I hope this information is useful to you — it should give you a solid starting point for setting up multi-tenancy with MFEs in Tutor.

2 Likes

I am having issues setting up eox_tenant, i cant seem to create a tenant and create a tenant link, the doc in the readme is not explanatory, please if you can help or send any link to help, i will appreciate.

The configuration is fairly simple, and I’ll share some basic steps I implemented on Redwood (note: I’m not sure if there are any differences in Sumac):

  1. Install the plugin

    pip install eox-tenant==your_version

  2. Run migrations
    ./manage.py lms migrate

    This will create four models. The ones that matter for our use case are Route and TenantConfig.

  3. Create a Route
    Go to http://local.overhang.io:8000/admin/eox_tenant/route/ and click Add route.
    This model has two fields:

    • domain: This should be the tenant domain, e.g., local.overhang.io (without http or port).

    • config: This refers to the ID of the associated TenantConfig.

    When you save the route, it will prompt you to create a TenantConfig. Simply enter a tenant key and save.

  4. Configure the TenantConfig
    Edit the TenantConfig you just created and add your custom LMS settings in JSON format. For example:

    { "EDNX_USE_SIGNAL": true, "PLATFORM_NAME": "local", "course_org_filter": ["edx"] }

  5. Repeat for another tenant
    You can repeat the above steps for another tenant like http://test.local.overhang.io:8000/ and provide a different configuration.


With that setup, you now have two sites running locally with different configurations. You can override any setting that is read at runtime. However, settings that are loaded during server startup—like routes in urls.py—cannot be modified this way, since eox-tenant relies on the request object to determine context.

Thanks for this reply, After adding this , when i visit test.local.edly.io, its just a blank screen, where am i to create the tenant that links to this

For some reason, this is not working

Everytime i visit test.local.edly.io,

I get an empty page. please what could i be doing wrong.

Your configuration looks ok, you can try changing the route and then visit test.local.edly.io, if you get the same result the issue is not eox-tenant, could be your server, anyway it’s hard to know what it’s happening without server logs

The log looks like this, I dont know if you can make any meaning from this, i really appreciate you help.

2025-07-30 17:47:39,411 INFO 23 [tracking] [user ] [ip ] logger.py:41 - {“name”: “/admin/jsi18n/”, “context”: {“user_id”: "“, “path”: “/admin/jsi18n/”, “course_id”: “”, “org_id”: “”, “enterprise_uuid”: “”}, “username”: ““, “session”: “", “ip”: "”, “agent”: “Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0”, “host”: “local.edly.io”, “referer”: “http://local.edly.io/admin/eox_tenant/route/11/change/”, “accept_language”: “en-US,en;q=0.5”, “event”: “{“GET”: {}, “POST”: {}}”, “time”: “2025-07-30T17:47:39.411329+00:00”, “event_type”: “/admin/jsi18n/”, “event_source”: “server”, “page”: null}
2025-07-30 17:47:43,994 INFO 23 [eox_tenant.signals] [user None] [ip None] signals.py:49 - Site local.edly.io, does not use eox_tenant signals
2025-07-30 17:47:43,999 INFO 23 [tracking] [user ] [ip ] logger.py:41 - {“name”: “/admin/eox_tenant/route/11/change/”, “context”: {“user_id”: "”, “path”: “/admin/eox_tenant/route/11/change/”, “course_id”: “”, “org_id”: “”, “enterprise_uuid”: “”}, “username”: “”, “session”: “", “ip”: "”, “agent”: “Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0”, “host”: “local.edly.io”, “referer”: “http://local.edly.io/admin/eox_tenant/route/11/change/”, “accept_language”: “en-US,en;q=0.5”, “event”: “{“GET”: {}, “POST”: {“csrfmiddlewaretoken”: [”"], “domain”: [“test.local.edly.io”], “config”: [“6”], “_save”: [“Save”]}}“, “time”: “2025-07-30T17:47:43.999615+00:00”, “event_type”: “/admin/eox_tenant/route/11/change/”, “event_source”: “server”, “page”: null}
2025-07-30 17:47:44,052 INFO 22 [eox_tenant.signals] [user None] [ip None] signals.py:49 - Site local.edly.io, does not use eox_tenant signals
2025-07-30 17:47:44,055 INFO 22 [tracking] [user ] [ip ] logger.py:41 - {“name”: “/admin/eox_tenant/route/”, “context”: {“user_id”: "
”, “path”: “/admin/eox_tenant/route/”, “course_id”: “”, “org_id”: “”, “enterprise_uuid”: “”}, “username”: "”, “session”: “”, “ip”: ““, “agent”: “Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0”, “host”: “local.edly.io”, “referer”: “http://local.edly.io/admin/eox_tenant/route/11/change/”, “accept_language”: “en-US,en;q=0.5”, “event”: “{“GET”: {}, “POST”: {}}”, “time”: “2025-07-30T17:47:44.055292+00:00”, “event_type”: “/admin/eox_tenant/route/”, “event_source”: “server”, “page”: null}
2025-07-30 17:47:44,335 INFO 23 [eox_tenant.signals] [user None] [ip None] signals.py:49 - Site local.edly.io, does not use eox_tenant signals
2025-07-30 17:47:44,338 INFO 23 [tracking] [user ] [ip ] logger.py:41 - {“name”: “/admin/jsi18n/”, “context”: {“user_id”: "
”, “path”: “/admin/jsi18n/”, “course_id”: “”, “org_id”: “”, “enterprise_uuid”: “”}, “username”: "”, “session”: “”, “ip”: ““, “agent”: “Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0”, “host”: “local.edly.io”, “referer”: “http://local.edly.io/admin/eox_tenant/route/”, “accept_language”: “en-US,en;q=0.5”, “event”: “{“GET”: {}, “POST”: {}}”, “time”: “2025-07-30T17:47:44.338484+00:00”, “event_type”: “/admin/jsi18n/”, “event_source”: “server”, “page”: null}
2025-07-30 17:47:44,439 INFO 23 [eox_tenant.signals] [user None] [ip None] signals.py:49 - Site local.edly.io, does not use eox_tenant signals
2025-07-30 17:47:44,443 INFO 23 [tracking] [user ] [ip ] logger.py:41 - {“name”: “/theming/asset/images/favicon.ico”, “context”: {“user_id”: "
”, “path”: “/theming/asset/images/favicon.ico”, “course_id”: “”, “org_id”: “”, “enterprise_uuid”: “”}, “username”: "”, “session”: "”, “ip”: “***”, “agent”: “Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0”, “host”: “local.edly.io”, “referer”: “http://local.edly.io/admin/eox_tenant/route/”, “accept_language”: “en-US,en;q=0.5”, “event”: “{“GET”: {}, “POST”: {}}”, “time”: “2025-07-30T17:47:44.443300+00:00”, “event_type”: “/theming/asset/images/favicon.ico”, “event_source”: “server”, “page”: null}

Summary

This text will be hidden

Could it be the version of eox_tenant i am using, i am using v11.7.0 for my redwood installation, i have tried different version and i get same issue.

When i try to visit studio i get : Error: invalid_request

Invalid client_id parameter value.

If you have any direction to point me, i will appreciate, thank you!

The logs only shows requests to “http://local.edly.io/admin/eox_tenant/route/11/change/,however I understood that you are having issues with http://test.local.edly.io so those log doesn’t give any related information, on the other hand, you have to add http://local.edly.io to the toolkit-app redirect uris field to solve the studio issue

I added http://local.edly.io but it didnt work, i will go through the logs to try and sort out the issue.

Thanks for all your help.