This guide explains how to build a scalable, multi-persona SaaS (Admin, Seller, Shop) using AdonisJS 7 and Inertia.js (React). This architecture ensures isolated bundles, distinct layouts, and full TypeScript safety.
1. Directory Structure
Organize your frontend assets by persona:
inertia/
├── app/
│ ├── admin.tsx # Admin Entrypoint
│ ├── seller.tsx # Seller Entrypoint
│ └── shop.tsx # Shop Entrypoint
├── layouts/
│ ├── admin/ # Admin Layouts
│ ├── seller/ # Seller Layouts
│ └── shop/ # Shop Layouts
└── pages/
├── admin/ # Admin Pages
├── seller/ # Seller Pages
└── shop/ # Shop Pages
2. Global Root View Configuration
Instead of manually setting the root view in every route, use a functional rootView in config/inertia.ts. This automatically selects the correct HTML shell based on the URL.
// config/inertia.ts
import { defineConfig } from '@adonisjs/inertia'
const inertiaConfig = defineConfig({
rootView: (ctx) => {
if (ctx.request.url().startsWith('/admin')) return 'admin'
if (ctx.request.url().startsWith('/seller')) return 'seller'
if (ctx.request.url().startsWith('/shop')) return 'shop'
return 'inertia_layout' // Default
},
// ...
})
3. Entrypoint Definitions
Create dedicated entrypoints in inertia/app/. To keep TypeScript types working, we resolve pages from the root pages/ directory but wrap them in persona-specific layouts.
// inertia/app/admin.tsx
import { createRoot } from 'react-dom/client'
import { createInertiaApp } from '@inertiajs/react'
import { resolvePageComponent } from '@adonisjs/inertia/helpers'
import AdminLayout from '~/layouts/admin/AdminDefaultLayout'
const appName = 'Oribat | Admin'
createInertiaApp({
title: (title) => (title ? `${title} - ${appName}` : appName),
resolve: (name) => {
return resolvePageComponent(
`../pages/${name}.tsx`,
import.meta.glob('../pages/**/*.tsx'),
(page) => <AdminLayout children={page} />
)
},
setup({ el, App, props }) {
createRoot(el).render(<App {...props} />)
},
})
4. Edge Root Views
Each persona needs its own .edge file in resources/views/ to load the specific Vite bundle.
{{-- resources/views/admin.edge --}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title inertia>Oribat Admin</title>
@viteReactRefresh()
@vite(['inertia/app/admin.tsx'])
@inertiaHead()
</head>
<body class="admin-theme">
@inertia()
</body>
</html>
5. Vite Configuration
Register all entrypoints in vite.config.ts so they are bundled separately.
// vite.config.ts
export default defineConfig({
plugins: [
adonisjs({
entrypoints: [
'inertia/app/admin.tsx',
'inertia/app/seller.tsx',
'inertia/app/shop.tsx',
],
}),
],
})
6. Defining Routes
Use the full component path (including the folder) in your routes. This ensures that AdonisJS TypeScript generators recognize the components correctly.
// start/routes.ts
// Admin Group
router.group(() => {
router.get('/', async ({ inertia }) => {
return inertia.render('admin/Dashboard', { stats: { ... } })
})
}).prefix('/admin')
// Seller Group
router.group(() => {
router.get('/', async ({ inertia }) => {
return inertia.render('seller/StoreDashboard', { stats: { ... } })
})
}).prefix('/seller')
7. Shared Props & Middleware
To share data (like the authenticated user) across all entrypoints, use the InertiaMiddleware. In AdonisJS 7, this middleware is where you define your global state.
// app/middleware/inertia_middleware.ts
export default class InertiaMiddleware extends BaseInertiaMiddleware {
async share(ctx: HttpContext) {
return {
user: ctx.auth?.user,
flash: ctx.session?.flashMessages.all(),
}
}
}
TypeScript Augmentation
To get autocomplete in your React components, augment the SharedProps interface:
declare module '@adonisjs/inertia/types' {
export interface SharedProps extends InferSharedProps<InertiaMiddleware> {}
}
8. Title Management
Inertia takes over the title once the app loads.
- Edge Fallback: Use
<title inertia>in your.edgefiles for the initial loading state. - JS Branding: Set the
appNameandtitlecallback in your.tsxentrypoint to control the browser tab branding.
Key Benefits
- Bundle Optimization: Admins don't load customer-facing code, and vice versa.
- Layout Isolation: No complex conditional logic needed in a single master layout.
- Type Safety: Full auto-completion for component names and props across all entrypoints.
- Theme Control: Different CSS classes (e.g.,
admin-theme) can be applied to the body at the Edge level.
Top comments (0)