DEV Community

Ahsan Abrar
Ahsan Abrar

Posted on

Multi-Entrypoint SaaS Architecture with AdonisJS & Inertia + ReactJS

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
Enter fullscreen mode Exit fullscreen mode

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
  },
  // ...
})
Enter fullscreen mode Exit fullscreen mode

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} />)
  },
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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',
      ],
    }),
  ],
})
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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(),
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Augmentation

To get autocomplete in your React components, augment the SharedProps interface:

declare module '@adonisjs/inertia/types' {
  export interface SharedProps extends InferSharedProps<InertiaMiddleware> {}
}
Enter fullscreen mode Exit fullscreen mode

8. Title Management

Inertia takes over the title once the app loads.

  1. Edge Fallback: Use <title inertia> in your .edge files for the initial loading state.
  2. JS Branding: Set the appName and title callback in your .tsx entrypoint to control the browser tab branding.

Key Benefits

  1. Bundle Optimization: Admins don't load customer-facing code, and vice versa.
  2. Layout Isolation: No complex conditional logic needed in a single master layout.
  3. Type Safety: Full auto-completion for component names and props across all entrypoints.
  4. Theme Control: Different CSS classes (e.g., admin-theme) can be applied to the body at the Edge level.

Top comments (0)