Skip to content

Installation

Install Adonia, generate its host integration, and verify the result.

The installer is one command with nine steps, every one of them idempotent: re-running it produces a byte-identical tree and never overwrites a file you own. The implementation orchestrates, one module per step — and its conformance suite is packages/devtools/tests/configure.spec.ts.

Requirements

Adonia packages install their own implementation dependencies; host-framework integrations remain peer dependencies so the application owns the AdonisJS, React, Vine, and Lucid instances.

Requirement Range Notes
Node.js ≥ 24 the AdonisJS v7 baseline (§1.2)
TypeScript ≥ 5.9 required for the generated types; the packages build against ~5.9.2
tsconfig.json at the app root the installer throws without it, since its codemods parse your TypeScript through it

Required peers of @adonia/core:

Package Range
@adonisjs/core ^7.0.0
@adonisjs/lucid ^22.0.0
@vinejs/vine ^4.0.0

Optional peers of @adonia/core (declared optional in peerDependenciesMeta; absent means the corresponding feature is inert, not broken):

Package Range Enables
@adonisjs/inertia ^4.0.0 rendering panels at all — required in practice for the UI
@adonisjs/auth ^10.0.0 panel guards and the login page
@adonisjs/bouncer ^4.0.0 policy-backed authorization; without it every check falls through to config.authorization.fallback
@adonisjs/session ^8.0.0 flash messages, session-backed table state
luxon ^3.0.0 date and datetime fields
@adonisjs/cache ^2.1.0 widget result caching for widgets that declare cacheFor()

Peers of @adonia/ui: react and react-dom at ^19.0.0, @inertiajs/react at ^2.0.0, @adonisjs/inertia at ^4.0.0.

Two deltas against the §1.2 matrix, documented from the manifests rather than the spec: @adonia/ui requires React 19, not “≥ 18”; and tailwindcss is not declared as a peer of @adonia/ui — the installer adds tailwindcss and @tailwindcss/vite at ^4.3.3 to the application’s devDependencies when it has none.

The one command

node ace add @adonia/core

add installs the package and then runs its configure hook. To re-run only the hook — after a failed install, after deleting a scaffolded file you want back, or after upgrading:

node ace configure @adonia/core

Both are safe to repeat. --no-install skips the package-manager call and prints the list of packages to add by hand instead.

What the installer does

The steps below are the order engine.ts executes them in. “Skipped” is a real outcome that appears in the post-install report, not a silent no-op.

0 — Sibling packages. Before the engine runs, the configure hook reads the app’s package.json and installs @adonia/ui (a dependency) and @adonia/devtools (a devDependency), filtering out whichever the app already declares in either map — so workspace setups that link them are left alone. With --no-install it lists them instead of installing. If the install fails, it warns, sets exit code 1 and stops: none of the steps below run.

The engine then asserts a tsconfig.json exists at the app root and throws a pointed error if it does not.

1 — adonisrc.ts. Four edits, each guarded:

  • import { indexAdoniaResources } from '@adonia/devtools/hooks', merged into an existing declaration for that specifier if there is one.
  • indexAdoniaResources() inserted into hooks.init immediately after the indexEntities() element — appended at the end if the app has no indexEntities() call, and the hooks / init structures are created when missing. Order matters: the codegen hook reads what entity indexing produced.
  • the provider @adonia/core/providers/adonia_provider, added through the assembler’s RcFileTransformer, which dedupes by module path.
  • the Ace commands entry @adonia/devtools/commands, same mechanism (the transformer creates the commands array if the app has none). Without it node ace adonia:doctor and every other Adonia generator simply does not exist in your app.

All four in-memory edits are formatted and saved in a single write, so the file round-trips to identical bytes on a second run. When everything is already present the file is reported as adonisrc.ts (provider, commands, and init hook already registered).

2 — config/adonia.ts published verbatim from a stub. An existing file is never touched.

3 — app/adonia/panels/admin_panel.ts published verbatim from a stub. Same rule.

3b — .adonisjs/adonia/{resources.ts,descriptor.d.ts,manifest.json} seeded. If .adonisjs/adonia/resources.ts already exists the whole step is skipped. Otherwise the engine scans app/adonia/resources for .ts files (ignoring .d.ts): with resources present it runs the full codegen pipeline; with none it writes an empty registry, an empty ambient descriptor.d.ts, and a manifest of { "protocolVersion": 1, "generatedAt": null, "resources": [], "panels": [] }.

This step exists because the scaffolded panel statically imports the generated registry, while the indexAdoniaResources() hook only runs under the assembler (serve, test, build). Without a seed file the first plain ace command after install — node ace migration:run, node ace repl — dies with ERR_MODULE_NOT_FOUND.

4 — package.json. Adds the subpath import "#adonia/*": "./app/adonia/*.js". When the app declares no tailwindcss in either dependency map, adds tailwindcss and @tailwindcss/vite at ^4.3.3 to devDependencies. The file is re-serialized preserving its original indent unit and trailing-newline style, and written back only when something actually changed; an alias already pointing at the right target is reported skipped.

5 — Inertia pages and client entry. Seven stubs published, none overwritten: inertia/pages/adonia/resource_index.tsx, resource_form.tsx, resource_detail.tsx, dashboard.tsx, login.tsx, error.tsx, and inertia/adonia.ts.

6 — Inertia and Tailwind wiring.

  • inertia/app.tsx gains the side-effect imports import './adonia' and import '@adonia/ui/styles.css', appended after the last existing import, each only when absent. Formatted and saved only if something was added; otherwise reported skipped. A missing inertia/app.tsx produces a warning telling you to wire both imports yourself.

  • inertia/app.tsx’s app-wide layout is routed through withAdoniaLayout from @adonia/ui. The inertia-react starter passes a layout as the third argument of resolvePageComponent, which @adonisjs/inertia assigns to every resolved page — including the packaged adonia/* pages, which already render their own PanelShell chrome and toaster because the packaged pages are self-contained. Left unguarded you get the app header around the panel sidebar and two of every flash message. The rewrite is structural (ts-morph) and recognizes the starter’s shape only:

    resolvePageComponent(
      `./pages/${name}.tsx`,
      import.meta.glob('./pages/**/*.tsx'),
      withAdoniaLayout(name, (page: ReactElement) => <Layout children={page} />)
    )

    An entry that applies no layout is left alone. An entry that does something else — a hand-assigned page.default.layout, a resolve that is not an inline function — is not silently skipped: the post-install report warns with the exact edit to apply, and node ace adonia:doctor’s inertia-layout check keeps flagging it until you do.

  • inertia/css/app.css gains @source '../../node_modules/@adonia/ui';, appended unless the file already contains both @source and @adonia/ui. This directive is mandatory — without it Tailwind v4 never sees the class names inside @adonia/ui and packaged pages render unstyled with no warning. When the app had no Tailwind, @import 'tailwindcss'; is prepended first. A missing CSS entry is a warning, not a failure.

  • vite.config.ts gets the @tailwindcss/vite plugin registered — only when the app had no Tailwind before this run. Already mentions @tailwindcss/vite? Skipped. Missing? Warning.

7 — Panel-access and tenant ejection seams. Publishes app/middleware/adonia_panel_access_middleware.ts and app/middleware/adonia_tenant_middleware.ts (never overwritten), then edits start/kernel.ts — or whichever directory the assembler reports as start — inserting:

'adonia.panel-access': () => import('#middleware/adonia_panel_access_middleware')
'adonia.tenant': () => import('#middleware/adonia_tenant_middleware')

Both properties live in the router.named({ … }) object bound to middleware. Keys are matched in bare, single-quoted and double-quoted form, so a second run inserts nothing. If start/kernel.ts is missing, or has no router.named({ … }) call, the step warns and asks you to register the names manually.

8 — Action persistence migrations. Publishes two independent migrations: database/migrations/<timestamp>_create_adonia_action_events_table.ts for the audit log and database/migrations/<timestamp>_create_adonia_action_batches_table.ts for owned queue progress and retry-idempotent chunks. Idempotency cannot key off a fixed filename, because both make:migration and this step prefix timestamps. The migrations directory is scanned for each suffix independently, so upgrading an app that already has the events migration still publishes the new batches migration. Existing files are never altered.

9 — The report. Every step records what it created, modified, skipped (and why), warned about, and added to package.json. The hook prints those buckets followed by the next steps — run your package manager, run node ace migration:run, create a resource with node ace adonia:resource <Model>, start node ace serve --hmr and visit /admin.

The resulting footprint of a clean install on the official inertia-react starter:

.adonisjs/adonia/
├── descriptor.d.ts                                   created (seed)
├── manifest.json                                     created (seed)
└── resources.ts                                      created (seed)
app/
├── adonia/panels/admin_panel.ts                      created
├── middleware/adonia_panel_access_middleware.ts      created
└── middleware/adonia_tenant_middleware.ts            created
config/adonia.ts                                      created
database/migrations/
├── <ts>_create_adonia_action_events_table.ts         created
└── <ts>_create_adonia_action_batches_table.ts        created
inertia/
├── adonia.ts                                         created
├── app.tsx                                           modified — 2 imports + layout guard
├── css/app.css                                       modified — @source (+ @import)
└── pages/adonia/
    ├── dashboard.tsx                                  created
    ├── error.tsx                                      created
    ├── login.tsx                                      created
    ├── resource_detail.tsx                            created
    ├── resource_form.tsx                              created
    └── resource_index.tsx                             created
adonisrc.ts                                           modified — provider, commands, hook
package.json                                          modified — #adonia/*, Tailwind
start/kernel.ts                                       modified — adonia.panel-access, adonia.tenant
vite.config.ts                                        modified — tailwindcss()

What each artifact is for

config/adonia.ts is the framework-wide default set. Every value below is overridable per panel and per resource, most-specific wins. The stub writes the framework defaults explicitly so the whole surface is visible in one place, and defineConfig validates the merged result at boot — a bad pagination or a negative perPage fails startup rather than the first request.

import { defineConfig } from '@adonia/core'

export default defineConfig({
  panels: undefined,

  defaults: {
    perPage: 25,
    perPageOptions: [10, 25, 50, 100],
    pagination: 'offset',
    dateFormat: 'DD MMM YYYY',
    dateTimeFormat: 'DD MMM YYYY HH:mm',
    redirectAfterCreate: 'edit',
    redirectAfterUpdate: 'stay',
    actionChunkSize: 200,
  },

  authorization: {
    fallback: 'deny',
    devFallback: 'allow',
  },

  search: {
    driver: 'database',
    minQueryLength: 2,
    globalSearchLimitPerResource: 5,
  },

  uploads: {
    disk: undefined,
    maxSizeMb: 10,
  },

  telemetry: { spans: true },
})

panels: undefined means auto-discovery from app/adonia/panels/*; set it to an explicit list of module specifiers to override that.

The provider registration (@adonia/core/providers/adonia_provider in adonisrc.ts) is what boots the PanelManager, loads panel definitions and registers each panel’s routes. Nothing about Adonia is reachable without it.

The adonia.panel-access and adonia.tenant middleware entries in start/kernel.ts are ejection seams. The route registrar applies its own instances to panel route groups, so the entries are not what makes ordinary panel routes safe. They let host routes reuse the same gates and provide subclasses for application-specific overrides. Everyday panel entry rules belong on Panel.make('admin').access(…); tenant lookup belongs on Panel.make('admin').tenant(…), not in the middleware subclasses.

The init hook indexAdoniaResources() in adonisrc.ts hooks.init is the codegen entry point. Under the assembler it rescans app/adonia/** and regenerates .adonisjs/adonia/* on every run and on every HMR cycle. See codegen.

app/adonia/panels/admin_panel.ts is the scaffolded panel: Panel.make('admin').path('/admin').guard('web').resources(registry), where registry is the generated one. It is ordinary code — rename it, add branding, add navigation groups, add a second panel beside it.

inertia/adonia.ts is the client entry, imported for side effects from inertia/app.tsx. It exports adonia({}) and is where you register custom field, cell and page components and theme overrides. inertia/pages/adonia/*.tsx are three-line re-exports of the packaged pages; they exist so the pages participate in your Vite build and your indexPages scan. node ace adonia:eject resource_index replaces one of them with the full source when you want to take that page over — the ejected copy still imports from @adonia/ui, so upstream fixes keep arriving.

.adonisjs/adonia/ holds three generated artifacts: resources.ts, a registry of slug → lazy import that the panel consumes (code-split, no runtime globbing); descriptor.d.ts, the ambient per-resource descriptor and row types, pulled in by a triple-slash reference at the top of resources.ts so no tsconfig.json edit is needed; and manifest.json, which adonia:doctor reads to learn which panels and slugs exist. The installer only seeds them. Do not edit them; the hook owns them from the first assembler run onward.

The action persistence migrations create the three tables Adonia owns: the action audit log, execution batches, and retry-idempotent batch chunks. They are published rather than run by the framework so your app keeps control of when its schema changes and of the files’ contents.

Upgrading from 0.1.x-alpha to 0.2.0-beta

  1. Upgrade @adonia/core, @adonia/ui, and @adonia/devtools together. Their versions are fixed as one release set.
  2. Re-run node ace configure @adonia/core. The idempotent installer leaves owned files unchanged and publishes the new <timestamp>_create_adonia_action_batches_table.ts migration even when the older action events migration already exists.
  3. Review and run node ace migration:run, then run node ace adonia:doctor. Declared actions require all three action tables; the doctor reports any missing migration.
  4. Add @adonisjs/cache only when widgets use cacheFor(). Add @adonia/queue-bullmq plus BullMQ for queued actions, or @adonia/meilisearch for the reference search driver. Both adapters are optional.
  5. Regenerate .adonisjs/adonia/* by running an assembler command such as node ace serve --hmr, node ace test, or node ace build.

Protocol version 1 and existing resource declarations remain valid. Actions, widgets, lenses, relation managers, optimistic locking, domains, and relation fields were added without changing the frozen descriptor-node skeleton.

What it does NOT do

  • Install the Tailwind packages it adds. Step 4 writes them into package.json; the report tells you to run your package manager.
  • Run the migration. node ace migration:run is yours to run, after you have read the published file.
  • Enable SSR. It stays opt-in and requires both vite.config.ts (inertia({ ssr: … })) and config/inertia.ts (ssr.enabled); the installer never flips one silently, it just points at the pair.
  • Write the @adonia/ui/tailwind token preset into your Tailwind config. The preset maps design tokens and declares no content globs, so Adonia’s own components are already covered by the mandatory @source directive. Importing the preset is offered as a next step, for styling panel-adjacent pages of your own.
  • Overwrite anything. The seven published files are skipped when present, the migration is skipped when any copy exists, and every codemod checks before it inserts.
  • Create a resource. Use node ace adonia:resource <Model>, which derives a first schema from the model’s columns.
  • Regenerate .adonisjs/adonia/*. After the seed, that is the init hook’s job.
  • Configure authentication. The scaffolded panel names the web guard; that guard has to exist in your config/auth.ts.
  • Scope cookies for domain-mounted panels. A panel on its own subdomain needs the session and XSRF cookie domain set deliberately — the doctor’s cookie-domain check will tell you when it is wrong.

Verifying the install

node ace adonia:doctor

The doctor never boots the application: every finding comes from the file system and from static analysis of your own TypeScript, which is the point, because most of what it diagnoses is a reason the app fails to boot. Four of its checks map directly onto install steps — init-hook (step 1), panel-access-middleware (step 7), stale-codegen (step 3b), and action-events-table (step 8) — and the rest cover the sharp edges the installer deliberately left to you. Exit code is 1 when any check fails. See node ace adonia:doctor.

Then start the dev server and open the panel:

node ace serve --hmr

/admin renders an empty panel until you add a resource. That is Resources.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close