Modularity in multi-tenant vue app

Hi,

we’re developping a multi tenant application with some common features and some tenant specific features. Routes and navigation for commons are hard-coded and those for tenant specific are loaded dynamically from database (each tenant has its own database). As we want the UX to be coherent, common and tenant specific features both are part of the same Vue app and here comes the issue with modularity / robustness.

Although I load the routes and nav links dynamically, I still need to have all the tenant specific components inside the common core (and therefore in the same repo), which kind of destroys the idea of modularity. The architecture desired would be to have the common app with some common routes and features and then n “subapps” for each tenant which would launch only for custom routes inside router-view component of the core app. Those “subapps” would be loaded asynchronously and they would have its own routing (possibly managed from the core app, because the routes are loaded dynamically).

Is this achievable? In case some of it doesn’t make sense, i can provide with code snippets or explain it in more depth.

Thanks!

Edit 1: By “subapps” I mean tenant specific components with their routes and tenant specific vuex modules

I’m not sure if it’s the same thing as you want, but this is what I have done on an app that I’m working on to make it easier to share development. Might give you some ideas.

TL;DR
I only load up subapps that are enabled, and all the individual subapps manage their own routing/components/store and are loaded asynchronously, and bundled into the same js file.

Structure:

apps
  |-- users
    |-- components
    |-- features
      |-- billing
        |-- components
          |-- ListScreen.vue
          |-- CreateScreen.vue
        |-- routes.js
        |-- store
          |-- mutations.js
          |-- actions.js
          |-- index.js
      |-- services
      |-- jobs
    |-- config.js
  |-- App2
  |-- App3
  |-- ...
  |-- index.js
root
  |-- Header.vue
  |-- Navigation.vue
  |-- ...
...

So each subapp has it’s own shared components, and then the individual parts of that app are broken down into features which control their own store and routing.

app/users/config.js

export default {
  // Is this app active?
  enabled: true,

  // App namespace
  ns: 'billing',

  // The name of this app
  name: 'Bills',

  // Alternative title
  title: 'My Bills',

  // Icon to use to identify this app
  icon: 'money',
};

apps/users/features/billing/routes.js

const ListScreen = () =>
  import(/* webpackChunkName: "users-billing" */ './components/ListScreen');

const CreateScreen = () =>
  import(/* webpackChunkName: "users-billing" */ './components/CreateScreen');


export default [
  {
    nav: true, // Is this route shown in the main nav
    icon: 'money',
    label: 'Bills',
    name: 'billing_list',
    path: 'bills',
    component: ListScreen,
  },
  {
    name: 'billing_create',
    path: 'billing',
    component: CreateScreen,
  },
];

The main part is the index.js in apps that loads up any enabled apps and creates the routing etc for the whole spa. This way I can disable any individual app by changing the value in the config.js, and no routes or components are loaded for it.

apps/index.js

let appRoutes = []; // eslint-disable-line
let appNavigation = []; // eslint-disable-line

const getAppRouting = () => appRoutes;
const getPrimaryNavigation = () => appNavigation;

const configRequire = require.context('apps', true, /\.\/[^/]+\/config\.js$/);
const routesRequire = require.context('apps', true, /\.\/[^/]+\/features\/[^/]+\/routes\.js$/);

configRequire.keys().forEach((configPath) => {
  const appPath = configPath.split('/')[1];
  const appConfig = configRequire(configPath).default;

  // Bail out if we have turned off for some reason
  if (!appConfig.enabled) return;

  const routes = [];
  const navigation = {
    ns: appConfig.ns,
    name: appConfig.name,
    title: appConfig.title,
    icon: appConfig.icon,
    children: [],
  };

  routesRequire.keys().forEach((routePath) => {
    const routeAppPath = routePath.split('/')[1];

    // Bail if not belonging to app
    if (appPath !== routeAppPath) return;

    routesRequire(routePath).default.forEach((route) => {
      // Update the name and path with the app namespace prefix
      routes.push({
        ...route,
        name: `${appConfig.ns}__${route.name}`,
        path: `/${appConfig.ns}/${route.path}`,
      });

      // Should this be shown in the nav?
      if (route.nav) {
        navigation.children.push({
          route: `${appConfig.ns}__${route.name}`,
          label: route.label,
          icon: route.icon,
        });
      }
    });
  });


  appRoutes = appRoutes.concat(routes);

  if (navigation) {
    // Make the app features alphabetical for nicencess
    navigation.children.sort((a, b) => (a.label > b.label) ? 1 : 0);

    appNavigation.push(navigation);
  }
});

// Make the apps alphabetical for niceness
appNavigation.sort((a, b) => (a.label > b.label) ? 1 : 0);

export {
  getAppRouting,
  getPrimaryNavigation,
};

Then I use the routes like this:

import { getAppRouting } from 'apps';

const router = new Router({
  mode: 'history',
  base: 'webapp',
  routes: [
    ...getAppRouting(),
    { path: '*', component: NotFoundComponent },
  ],
});
3 Likes

Travel app
Travel is something that gives us a break from our routine.
But is travel planning a tedious activity?
No, it isn’t. In fact, with a travel planning app, things have become easier because anyone can plan and book their trip using their phone. So the demand for travel apps will continue to grow, creating better opportunities for entrepreneurs and startups. Here’s more information about it on Devlight

Thanks for this interesting information!

Web software development is the process of creating web applications, websites, and other software programs that run on the web. This involves writing server-side application code and client-side application code for web browsers https://mlsdev.com/services/it-staff-augmentation.