---
title: "Introduction"
description: "Build secure AdonisJS admin panels from typed resources, schemas, and policies."
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.

# Introduction

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](/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](/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](/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.

```ts
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

- [Installation](/guide/install) — `node ace add @adonia/core`, step by step, and
  what every generated file is for.
- [Panels](/guide/panels) — the top-level unit: base path, guard, navigation.
- [Resources](/guide/resources) — the full resource API around the schema above.
- [Descriptor protocol v1](/protocol/v1) — the frozen server/client wire contract.

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