cjholowatyj's avatar

Configuring per-tenant Mix workflows for multi-tenant SaaS platform

[ Using Laravel ^8.40 / Inertia JS / Vue ^3.2.11 / Laravel Mix ^6.0.31 ]

I'm working on a multi-tenant SaaS platform for publishing client websites where each tenant has one or many Projects, each with their own mix workflow. Each tenant project has it's own unique routes for that tenant's domain, instead of the same routes for each individual tenant, which is the reason why I'm configuring parallel mix workflows (one workflow per project). I'm finding it challenging to configure the output directory structure exactly the way I want it and I've tried to figure it out based on documentation available online but I definitely feel like I'm missing something and I'm running around in circles...

My Vue components and Sass styles are stored in resources/{js|scss}/projects/{tenant}/ (including the pages sub-directory as well), and I'd like for Laravel Mix to output all of the compiled JS/CSS files to public/projects/{tenant}/{js|css}/ and have the mix-manifest.json file output to public/projects/{tenant} and have all of the "chunk'ed" page components output to the same folder as the other JS files... I'm not quite accomplishing this though... I've tried all manner of option combinations, but haven't found the winning set.

My webpack file (the {tenant} here is p017):

const mix = require('laravel-mix');
const path = require('path');

mix
	/* Setup */
	.setPublicPath('public/projects/p017')
	.disableNotifications()
	.autoload({jquery: ['$', 'window.jQuery']})
	.webpackConfig({
		resolve: {
			extensions: ['.js', '.json', '.vue', '.css', '.scss'],
			alias: {
				'@': path.resolve(__dirname, 'resources/js/'),
				'~': path.resolve(__dirname, 'resources/scss/')
			}
		},
		output: {
			chunkFilename: 'projects/p017/js/[name].js?id=[chunkhash]'
		},
		devtool: 'inline-source-map'
	})
	.options({
		postCss: [
			require('postcss-import'),
			require('tailwindcss')
		],
		processCssUrls: true,
		cssNano: {
			discardComments: {removeAll: true}
		}
	})
	/* Scripts */
	.js('resources/js/projects/p017/p017.js', 'projects/p017/js/app.js')
	.vue({version: 3, extractStyles: 'projects/p017/css/vue.css'})
	.extract('projects/p017/js/vendor.js')
	/* Styles */
	.sass('resources/scss/projects/p017/p017.scss', 'projects/p017/css/app.css')
	/* Cleanup */
	.sourceMaps()
	.version();

This webpack file results in the following (undesired) directory structure (all under public):

projects/p017/projects/p017/css/app.css
projects/p017/projects/p017/css/vue.css
projects/p017/projects/p017/js/app.js
projects/p017/projects/p017/js/manifest.js
projects/p017/projects/p017/js/resources_js_projects_p017_pages_Home_vue.js
projects/p017/projects/p017/js/vendor.js
projects/p017/mix-manifest.json

...and this is the desired directory structure (notice mix-manifest.json shouldn't move)...

projects/p017/css/app.css
projects/p017/css/vue.css
projects/p017/js/app.js
projects/p017/js/manifest.js
projects/p017/js/resources_js_projects_p017_pages_Home_vue.js
projects/p017/js/vendor.js
projects/p017/mix-manifest.json

If I remove the .setPublicPath() line, then the mix-manifest.json file lands in the public directory and conflicts with other tenants and if I set the output path for .js(), .vue(), .extract(), and .sass(), to public/projects/{tenant}/{js|css}/... then mix outputs all the JS/CSS files as /{js|css}/... (dropping the projects/{tenant}) which doesn't resolve to where they actually went.

Possible questions/solutions:

Is there a way to customize the mix-manifest.json directory or even the filename itself so I can prevent tenant conflicts?

Or, can I prepend public/ to each of the output directories in .js(), .vue(), .extract(), and .sass() without Laravel Mix stripping the projects/{tenant} from the output path?

Or, am I missing a better / all-encompasing set of documentation that describes in greater detail how to accomplish this?

0 likes
1 reply
martinbean's avatar

@cjholowatyj You’re really making life hard for yourself.

Just like the same back-end code should serve multiple tenants in a SaaS application, so should front-end code. If you have 100 clients or projects, do you really want to managing 100 Mix workflows containing multiple stylesheets and JavaScript files…? Even if you do, deployment is going to take ages as your server runs npm run prod for every tenant; my MacBook fans kicks in running NPM for one project; I dread to thing what a server’s going to do trying to do that 100 times over. It’s either going to eventually melt your CPU, or take hours to deploy every single change.

It completely defeats the point of building a SaaS and de-duplicating code if you then just have client-specific code like this. So instead look of templating and “theming”. I have a multi-tenant CMS but every website is served using the same Blade view files and styles built from the same stylesheet, and then I’ll use variables to change various things about the theme such as primary colour, background and body text colour, font family, etc.

Please or to participate in this conversation.