TabletopDOCS
Getting started

Install

Add tbtop/admin and its React client to an existing Laravel app, publish the host wiring, and finish the four steps the installer leaves to you.

Tabletop installs into a Laravel application you already have. It ships as two packages released in lockstep from one tag: tbtop/admin on Packagist (the PHP DSL, controllers and routes) and @tbtop/inertia-admin on npm (the React client that renders what the DSL describes). You install both, run one Artisan command that publishes three host files, then make four small edits in files the package deliberately does not touch.

Requirements

VersionWhy
PHP8.4 or newertbtop/admin requires php: ^8.4
Laravel11, 12 or 13illuminate/contracts: ^11.0 || ^12.0 || ^13.0
Inertia (server)inertiajs/inertia-laravel 3.1+Pulled in by Composer
React19Peer dependency of the client
Inertia (client)@inertiajs/react 3The admin entry calls createInertiaApp
Tailwind CSSv4, compiled by your Vite buildThe client ships a Tailwind source file, not built CSS
PHP GD extensionoptionalWithout it, uploaded images are stored but no resized variants (thumbnails) are generated

The host app does not need to be an Inertia app already, but it must be able to serve Inertia responses. That means an Inertia middleware in the web group, covered below.

Install the packages

Install both halves at the same version:

composer require tbtop/admin:^0.5.1
npm install --save-exact @tbtop/inertia-admin@0.5.1

The PHP package and the npm package describe one wire format between them, so keep their versions identical. --save-exact writes "0.5.1" to package.json instead of "^0.5.1". The Composer constraint ^0.5.1 allows later 0.5.x patches, so check composer.lock and package-lock.json agree after every update, or pin Composer exactly with tbtop/admin:0.5.1. When you upgrade one, upgrade the other in the same change.

The service provider (Tbtop\Admin\AdminServiceProvider) is auto-discovered. You do not register it by hand.

If your app does not have them yet, install the client-side build dependencies the admin entry uses:

npm install react@^19 react-dom@^19 @inertiajs/react@^3
npm install -D @vitejs/plugin-react laravel-vite-plugin tailwindcss @tailwindcss/vite

The client depends on @inertiajs/react 3. If your app is still on @inertiajs/react 2, upgrade it first. Otherwise npm installs a second copy for the client, your admin.tsx boots Inertia from one copy while the client reads page data from the other, and the panel does not render.

Run the migrations

The package ships four migrations for its media library (the tbtop_media_folders and tbtop_media tables). They load from the vendor directory, so you do not publish them. They run with your normal migrations:

php artisan migrate

To change them before they run, publish copies with php artisan vendor:publish --tag="tbtop-admin-migrations".

Publish the config

php artisan vendor:publish --tag="tbtop-admin-config"

This writes config/tbtop-admin.php. The key you need right away is panels, the list of panel classes. A panel that is not listed there has no routes. Everything else about a panel (prefix, guard, pages, locales, chrome) lives in the panel class, not in this file. The other keys cover content locales for translatable fields, the relation-search row cap and media-library storage. See Register a panel for what belongs where.

Publish the host wiring

php artisan admin:install

The command writes three files:

FileWhat it is
resources/views/admin.blade.phpThe root Blade view for admin pages. It loads only the admin entry, and sets the dark-mode class from the tbtop_theme cookie before first paint.
resources/js/admin.tsxThe admin JavaScript entry. It boots Inertia and resolves admin/page to the client's AdminPage component.
resources/css/admin.cssThe admin stylesheet. It imports @tbtop/inertia-admin/styles.css and adds @source '../../app/**/*.php' so Tailwind sees class names written in PHP.

If a file already exists, the command leaves it untouched and says so. Pass --force to overwrite it. It does not publish the config file. That is the separate vendor:publish step above.

The admin gets its own entry, view and stylesheet so that your public frontend does not load the admin bundle. The rich-text editor alone is around 270 KB.

Finish the four manual steps

After writing the files, admin:install prints four steps. It does not do them for you because they edit files the package does not own.

1. Add the admin entry to Vite's input list in vite.config.ts:

import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import laravel from "laravel-vite-plugin";
import { defineConfig } from "vite";

export default defineConfig({
	resolve: {
		dedupe: ["react", "react-dom", "@inertiajs/react"],
	},
	plugins: [
		laravel({
			input: ["resources/css/app.css", "resources/js/app.tsx", "resources/js/admin.tsx"],
			refresh: true,
		}),
		react(),
		tailwindcss(),
	],
});

The input line is the one admin:install prints. Keep whatever inputs your app already has and append resources/js/admin.tsx. If your app has no resources/js/app.tsx, do not list it, or the build fails on a missing input. admin.css is imported by admin.tsx, so it does not need its own input entry. The dedupe line stops Vite from bundling two copies of React or Inertia when a dependency resolves its own.

2. Point the panel at the published root view. The panel's root view defaults to app, so without this step admin pages render through your main layout and load the wrong bundle. In the panel's configure() method:

->rootView('admin')

3. Compile Tailwind v4 in the host build. Install tailwindcss and @tailwindcss/vite, and add tailwindcss() to the Vite plugins, as shown above.

4. Install the client and build. The installer prints npm install @tbtop/inertia-admin && npm run build, with no version. You already installed the pinned client above, so only build:

npm run build

Set up Inertia on the server

admin:install does not mention this, but the panel depends on it. Admin responses are Inertia responses, and form validation errors reach the fields through Inertia's shared errors prop, which the Inertia middleware provides. If your app has no Inertia middleware yet, create one and add it to the web group:

php artisan inertia:middleware
// bootstrap/app.php
use App\Http\Middleware\HandleInertiaRequests;

->withMiddleware(function (Middleware $middleware): void {
    $middleware->web(append: [HandleInertiaRequests::class]);
})

The panel also reads the signed-in user from a shared auth.user prop. Without it, the user menu (logout, language switcher) does not render. Authentication shows what to share.

Render the 404 page

The published admin.tsx resolves only admin/page. The panel also renders admin/error for unknown URLs under its prefix and for a findOrFail() that misses inside a page. With the stub unchanged, those requests throw [admin] No component found for page: admin/error in the browser. Add the second branch:

resolve: async (name) => {
	const mod = await import("@tbtop/inertia-admin");
	if (name === "admin/page") {
		return { default: mod.AdminPage };
	}
	if (name === "admin/error") {
		return { default: mod.AdminErrorPage };
	}
	throw new Error(`[admin] No component found for page: ${name}`);
},

Gotchas

  • Only one Inertia app per page load. The admin bundle resolves only admin/* components. An Inertia visit from an admin page to a page rendered by your main app.tsx, or the other way round, fails in the browser. Keep screens that admin users reach inside the panel, including the login page. Redirects that leave the panel should be full page loads.
  • Classes written in PHP need an @source line. If your pages live outside app/, or a package of yours writes admin classes, add one @source line per directory to resources/css/admin.css. Without it, those classes compile to nothing.
  • No Facade to import. The package's composer.json declares an Admin alias, but v0.5.1 ships no facade class behind it. Everything is configured through the panel class and page classes.

Next steps

On this page