Skip to content

Introduction

Build secure AdonisJS admin panels from typed resources, schemas, and policies.

Adonia is a code-first admin panel framework for AdonisJS v7. You describe a resource — a Lucid model bound to a declarative tree of fields — as a plain TypeScript class, and Adonia serves the paginated table, the create and edit forms, the detail page, and the authorization around all of them.

It is for teams already running AdonisJS v7 with Lucid and Inertia + React that need an internal back office, not a public product surface. It is not a site builder — configuration lives in code, never in the database — and not an API generator: it renders Inertia pages, and the JSON endpoints behind them are an implementation detail of those pages. Three ideas separate it from a hand-rolled CRUD scaffold.

One tree, three projections

A field is not a form control. It is one object carrying three concerns at once: the state contract (which model attribute it reads and writes), the projection rules (how it appears per context), and the validation contract. The same tree therefore compiles three ways — create, edit, and detail — instead of you maintaining a form definition, a table definition, and a read-only view that drift apart by the third sprint. In detail mode a text-input degrades to a text-entry; it bears no state, so it never reaches the validator or the fill set. See schema components.

The server compiles, the client renders

Every panel page is a JSON descriptor compiled per request from your resource class, rendered by generic React pages that resolve each node through a component registry. Descriptors are data, never code: a node is { type, key, props, children } and no function crosses the wire. Anything genuinely dynamic — dependent options, async search — is a capability URL the client calls back into.

That boundary is frozen. The wire format carries an explicit protocolVersion and v1 is closed to breaking change: see protocol v1. New field types extend it additively, which is what makes a custom field a server descriptor class plus a registered React component and nothing else.

Omission, not hiding

A node the viewer may not see is absent, not flagged invisible. There is no visible: false in the payload, because a client bug that renders a hidden node is then impossible by construction. Omission is total and happens in a single pass: the node disappears from the descriptor tree, from descriptor.stateKeys, from the compiled VineJS validator and from the fill set together. Nothing leaks the value through hydrated state, and nothing can be mass-assigned through a field the request was never allowed to see — the fill set is the compiled schema, so the allowlist cannot drift from what was rendered. Authorization covers the four gates that trigger it.

A resource, end to end

A complete resource. Dropped in app/adonia/resources/post_resource.ts, it produces the index table, the create and edit forms, and the detail page.

import { BaseResource, F } from '@adonia/core'
import type { SchemaBuilder, SchemaComponent } from '@adonia/core'
import Post from '#models/post'

export default class PostResource extends BaseResource<typeof Post> {
  static override model = Post
  static override slug = 'posts'
  static override labels = { singular: 'Post', plural: 'Posts' }

  schema(s: SchemaBuilder): SchemaComponent[] {
    return [
      s.section('Content').components([
        F.text('title').label('Title').required().maxLength(200).live(),
        F.text('slug').label('Slug').required().maxLength(220),
        F.textarea('body').label('Body').required(),
        F.select('status')
          .label('Status')
          .options({ draft: 'Draft', review: 'In review', published: 'Published' })
          .default('draft')
          .live(),
        F.datetime('publishedAt').label('Published at').visibleWhen('status', 'published'),
      ]),
    ]
  }
}

publishedAt demonstrates the third idea in miniature. While status is not published the field is not merely greyed out client-side: the server prunes it from the tree, the state, the validator and the fill set, and the client mirrors that decision rather than inventing its own.

Where to go next

  • Installationnode ace add @adonia/core, step by step, and what every generated file is for.
  • Panels — the top-level unit: base path, guard, navigation.
  • Resources — the full resource API around the schema above.
  • Descriptor protocol v1 — the frozen server/client wire contract.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close