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/coreadd 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/coreBoth 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 intohooks.initimmediately after theindexEntities()element — appended at the end if the app has noindexEntities()call, and thehooks/initstructures 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’sRcFileTransformer, which dedupes by module path. - the Ace commands entry
@adonia/devtools/commands, same mechanism (the transformer creates thecommandsarray if the app has none). Without itnode ace adonia:doctorand 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.tsxgains the side-effect importsimport './adonia'andimport '@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 missinginertia/app.tsxproduces a warning telling you to wire both imports yourself. -
inertia/app.tsx’s app-wide layout is routed throughwithAdoniaLayoutfrom@adonia/ui. The inertia-react starter passes a layout as the third argument ofresolvePageComponent, which@adonisjs/inertiaassigns to every resolved page — including the packagedadonia/*pages, which already render their ownPanelShellchrome 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, aresolvethat is not an inline function — is not silently skipped: the post-install report warns with the exact edit to apply, andnode ace adonia:doctor’sinertia-layoutcheck keeps flagging it until you do. -
inertia/css/app.cssgains@source '../../node_modules/@adonia/ui';, appended unless the file already contains both@sourceand@adonia/ui. This directive is mandatory — without it Tailwind v4 never sees the class names inside@adonia/uiand 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.tsgets the@tailwindcss/viteplugin 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
- Upgrade
@adonia/core,@adonia/ui, and@adonia/devtoolstogether. Their versions are fixed as one release set. - Re-run
node ace configure @adonia/core. The idempotent installer leaves owned files unchanged and publishes the new<timestamp>_create_adonia_action_batches_table.tsmigration even when the older action events migration already exists. - Review and run
node ace migration:run, then runnode ace adonia:doctor. Declared actions require all three action tables; the doctor reports any missing migration. - Add
@adonisjs/cacheonly when widgets usecacheFor(). Add@adonia/queue-bullmqplus BullMQ for queued actions, or@adonia/meilisearchfor the reference search driver. Both adapters are optional. - Regenerate
.adonisjs/adonia/*by running an assembler command such asnode ace serve --hmr,node ace test, ornode 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:runis yours to run, after you have read the published file. - Enable SSR. It stays opt-in and requires both
vite.config.ts(inertia({ ssr: … })) andconfig/inertia.ts(ssr.enabled); the installer never flips one silently, it just points at the pair. - Write the
@adonia/ui/tailwindtoken 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@sourcedirective. 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
webguard; that guard has to exist in yourconfig/auth.ts. - Scope cookies for domain-mounted panels. A panel on its own subdomain needs the
session and XSRF cookie
domainset deliberately — the doctor’scookie-domaincheck will tell you when it is wrong.
Verifying the install
node ace adonia:doctorThe 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.