Multi-tenant Next.js at any scale
Michele Riva
Senior Software Engineer @ViacomCBS
Book Author @Packt
Michele Riva
Senior Software Engineer @ViacomCBS
Book Author @Packt
Michele Riva
Senior Software Engineer @ViacomCBS
Book Author @Packt
What is multi-tenancy?
Single tenant
Separate Databases
Single Database, Separate Schemas
Instance Replication via Kubernetes
Kubernetes Cluster
Pod
Pod
Pod
Pod
Pod
Reverse Proxy
Cost reduction
Easier monitoring
Monorepo friendly
Reuse components and scripts
One pipeline to rule 'em all
Advantages of a multi-tenant architecture
Why Next.js?
Ease of use
We all love React
Built-in features
Easy onboarding
Great community
The dream
Next.js app
node_modules/
public/
components/
pages/
comedycentral/
paramount/
nickelodeon/
mtv/
The problem
Next.js currently supports only one website at the time
Possible approach #1
Instance replication + reverse proxy
Container
Reverse Proxy
Possible approach #2
Instance replication via Kubernetes
Kubernetes Cluster
Pod
Pod
Pod
Pod
Reverse Proxy
Possible approach #3
Move the reverse-proxy at the web server level
Krabs
Express.js middleware for multi-tenant Next.js applications
github.com/micheleriva/krabs
Next.js app
node_modules/
public/
components/
pages/
comedycentral/
paramount/
nickelodeon/
mtv/
server.js
.krabs.config.js
const express = require('express');
const next = require('next');
const krabs = require('krabs').default;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
async function main() {
try {
await app.prepare();
const handle = app.getRequestHandler();
const server = express();
server
.get('*', (req, res) => krabs(req, res, handle, app))
.listen(3000, () => console.log('server ready'));
} catch (err) {
console.log(err.stack);
}
}
main();
server.js
module.exports = {
tenants: [
{
name: 'comedycentral',
domains: [
{
development: /dev\.[a-z]*\.local\.comedycentral\.com/, // Regex supported!
stage: 'stage.comedycentral.com',
production: 'comedycentral.com',
},
],
},
{
name: 'paramount',
domains: [
{
development: 'local.paramountnetwork.com',
stage: 'stage.paramountnetwork.com',
production: /paramountnetwork\.[com|it|fr]/, // Regex supported!
},
],
},
],
// ...
};
.krabs.config.js
Next.js app
pages/
comedycentral/
server.js
.krabs.config.js
index.js
shows/
[show].js
paramount/
index.js
movies/
[movie].js
about.js
Next.js app
pages/
comedycentral/
server.js
.krabs.config.js
index.js
shows/
[show].js
paramount/
index.js
movies/
[movie].js
about.js
www.comedycentral.com
www.comedycentral.com/shows/southpark
Next.js app
pages/
comedycentral/
server.js
.krabs.config.js
index.js
shows/
[show].js
paramount/
index.js
movies/
[movie].js
about.js
www.comedycentral.com
www.comedycentral.com/shows/southpark
www.paramountnetwork.com
www.paramountnetwork.com/movies/titanic
Handling layouts using different libraries and technologies
import ComedyCentralLayout from '../layouts/comedycentral';
import ParamountLayout from '../layouts/paramount';
import NickelodeonLayout from '../layouts/nickelodeon';
import MtvLayout from '../layouts/mtv';
const layout = {
comedycentral: ComedyCentralLayout,
paramount: ParamountLayout,
nickelodeon: NickelodeonLayout,
mtv: MtvLayout
};
function App({ Component, pageProps }) {
const TenantLayout = layout[pageProps.tenant];
return (
<TenantLayout>
<Component {...pageProps} />
</TenantLayout>
);
}
App.getInitialProps = async ({ Component, ctx }) => {
const tenant = ctx?.req?.tenant?.name;
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return {
pageProps: { ...pageProps, tenant }
};
};
export default App;
_app.js
import { ChakraProvider } from '@chakra-ui/react';
import { Box } from '@chakra-ui/react';
import Menu from '../Menu';
function Layout(props) {
return (
<ChakraProvider>
<Box bg="cyan.50" minHeight="100vh">
<Menu />
<Box maxWidth="90vw" width={1200} margin="auto" pt={10}>
{props.children}
</Box>
</Box>
</ChakraProvider>
);
}
export default Layout;
layouts/comedycentral/index.js
White-label component library
Comedy Central theme
Paramount theme
Nickelodeon theme
Mtv theme
const themes = {
comedycentral: {
background: black,
color: white,
// ...
},
paramount: {
background: blue,
color: white,
// ...
},
nickelodeon: {
background: orange,
color: white,
// ...
},
mtv: {
background: grey,
color: white,
// ...
},
}
export default themes;
import { ThemeProvider } from 'styled-components';
import themes from '../styles/themes';
function App({ Component, pageProps }) {
const tenantTheme = themes[pageProps.tenant];
return (
<ThemeProvider theme={tenantTheme}>
<Component {...pageProps} />
</ThemeProvider>
);
}
App.getInitialProps = async ({ Component, ctx }) => {
const tenant = ctx?.req?.tenant?.name;
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
return {
pageProps: { ...pageProps, tenant }
};
};
export default App;
_app.js
Speedy demo!
Downsides of using Krabs
- Needs a custom server
- Cannot be deployed to Vercel
- No SSG + ISR
- _document.js and _app.js are the same for all websites
Downsides of using Krabs
- Needs a custom server
- Cannot be deployed to Vercel
- No SSG + ISR
- _document.js and _app.js are the same for all websites
How does Krabs scale?
Just like a common Express.js server.
What are Krabs use cases?
creative agencies
consultancy companies
big corporates
SASS products
Any alternative?
// Custom server
const fastify = require('fastify')()
fastify
.register(require('fastify-nextjs'))
.after(() => {
fastify.next('/hello')
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on http://localhost:3000')
})
// /pages/hello.js
export default () => <div>hello world</div>
const fastify = require('fastify')({
trustProxy: true
})
fastify
.register(require('fastify-nextjs'))
.after(() => {
fastify.next('/hello', (app, req, reply) => {
const { hostname } = req;
if (hostname === 'paramountnetwork.com') {
app.render(req.raw, reply.raw, '/paramount/hello', req.query, {})
}
else if (hostname === 'mtv.com') {
app.render(req.raw, reply.raw, '/mtv/hello', req.query, {})
}
else {
app.render(req.raw, reply.raw, '/_error', req.query, {})
}
})
})
fastify.listen(3000, err => {
if (err) throw err
console.log('Server listening on http://localhost:3000')
})
Some final tips
1) Keep it simple
Do you really need it?
2) Think template-based
3) Make it stateless
@MicheleRiva
@mitch_sleva
@MicheleRivaCode
/in/MicheleRiva95
www.micheleriva.it
https://rwnjs.com/order/amazon
https://rwnjs.com/order/packt
Multi-tenant Next.js at any scale
By Michele Riva
Multi-tenant Next.js at any scale
Slides for my Codemotion talk about multi-tenancy in Next.js
- 1,637