---
title: "Installation"
description: "Install Adonia, generate its host integration, and verify the result."
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Installation

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

```sh
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:

```sh
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:

  <!-- @sample-preamble
  import type { ComponentType, ReactElement } from 'react'
  import { withAdoniaLayout } from '@adonia/ui'
  import { resolvePageComponent } from '@adonisjs/inertia/helpers'
  declare const name: string
  declare const Layout: ComponentType<{ children: ReactElement }>
  declare global {
    interface ImportMeta {
      glob(pattern: string): Record<string, () => Promise<unknown>>
    }
  }
  -->

```tsx
  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:

```text
'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:

```text
.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.

```ts
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](/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

```sh
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`](/doctor).

Then start the dev server and open the panel:

```sh
node ace serve --hmr
```

`/admin` renders an empty panel until you add a resource. That is
[Resources](/guide/resources).

Source: https://adonia.pages.dev/guide/install/index.mdx
